From: Filip Schauer <f.schauer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox v4 02/15] add proxmox-oci crate
Date: Mon, 8 Sep 2025 17:02:05 +0200 [thread overview]
Message-ID: <20250908150224.155373-3-f.schauer@proxmox.com> (raw)
In-Reply-To: <20250908150224.155373-1-f.schauer@proxmox.com>
This crate can parse an OCI image tarball and extract its rootfs. Layers
are applied in sequence, but an overlay filesystem is currently not
used.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v3:
* correctly handle whiteout edge cases
* parse_and_extract_image: add argument for CPU architecture
Changed since v2:
* remove reachable unwraps & refactor code
* increase hasher buffer size from 4096 to 32768 (matching internal
sha2::Digest buffering)
* preserve permissions and xattrs during rootfs extraction
* handle whiteouts & opaque whiteouts
Cargo.toml | 1 +
proxmox-oci/Cargo.toml | 22 +++
proxmox-oci/debian/changelog | 5 +
proxmox-oci/debian/control | 45 +++++
proxmox-oci/debian/debcargo.toml | 7 +
proxmox-oci/src/lib.rs | 324 +++++++++++++++++++++++++++++++
proxmox-oci/src/oci_tar_image.rs | 144 ++++++++++++++
7 files changed, 548 insertions(+)
create mode 100644 proxmox-oci/Cargo.toml
create mode 100644 proxmox-oci/debian/changelog
create mode 100644 proxmox-oci/debian/control
create mode 100644 proxmox-oci/debian/debcargo.toml
create mode 100644 proxmox-oci/src/lib.rs
create mode 100644 proxmox-oci/src/oci_tar_image.rs
diff --git a/Cargo.toml b/Cargo.toml
index ce249371..ff12d7b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ members = [
"proxmox-network-api",
"proxmox-network-types",
"proxmox-notify",
+ "proxmox-oci",
"proxmox-openid",
"proxmox-product-config",
"proxmox-resource-scheduling",
diff --git a/proxmox-oci/Cargo.toml b/proxmox-oci/Cargo.toml
new file mode 100644
index 00000000..4daff6ab
--- /dev/null
+++ b/proxmox-oci/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "proxmox-oci"
+description = "OCI image parsing and extraction"
+version = "0.1.0"
+
+authors.workspace = true
+edition.workspace = true
+exclude.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+flate2.workspace = true
+oci-spec = "0.8.1"
+sha2 = "0.10"
+tar.workspace = true
+thiserror = "1"
+zstd.workspace = true
+
+proxmox-io.workspace = true
diff --git a/proxmox-oci/debian/changelog b/proxmox-oci/debian/changelog
new file mode 100644
index 00000000..754d06c1
--- /dev/null
+++ b/proxmox-oci/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-oci (0.1.0-1) bookworm; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 28 Apr 2025 12:34:56 +0200
diff --git a/proxmox-oci/debian/control b/proxmox-oci/debian/control
new file mode 100644
index 00000000..f33331c5
--- /dev/null
+++ b/proxmox-oci/debian/control
@@ -0,0 +1,45 @@
+Source: rust-proxmox-oci
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-flate2-1+default-dev <!nocheck>,
+ librust-oci-spec-0.8+default-dev (>= 0.8.1-~~) <!nocheck>,
+ librust-proxmox-io-1+default-dev (>= 1.2.0-~~) <!nocheck>,
+ librust-sha2-0.10+default-dev <!nocheck>,
+ librust-tar-0.4+default-dev <!nocheck>,
+ librust-thiserror-1+default-dev <!nocheck>,
+ librust-zstd-0.13+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.0
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-oci
+Rules-Requires-Root: no
+
+Package: librust-proxmox-oci-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-flate2-1+default-dev,
+ librust-oci-spec-0.8+default-dev (>= 0.8.1-~~),
+ librust-proxmox-io-1+default-dev (>= 1.2.0-~~),
+ librust-sha2-0.10+default-dev,
+ librust-tar-0.4+default-dev,
+ librust-thiserror-1+default-dev,
+ librust-zstd-0.13+default-dev
+Provides:
+ librust-proxmox-oci+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0-dev (= ${binary:Version}),
+ librust-proxmox-oci-0+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1.0+default-dev (= ${binary:Version})
+Description: OCI image parsing and extraction - Rust source code
+ Source code for Debianized Rust crate "proxmox-oci"
diff --git a/proxmox-oci/debian/debcargo.toml b/proxmox-oci/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-oci/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-oci/src/lib.rs b/proxmox-oci/src/lib.rs
new file mode 100644
index 00000000..cf0e4271
--- /dev/null
+++ b/proxmox-oci/src/lib.rs
@@ -0,0 +1,324 @@
+use std::collections::HashMap;
+use std::fs::{read_dir, remove_dir_all, remove_file, File};
+use std::io::{Read, Seek, SeekFrom};
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+
+use flate2::read::GzDecoder;
+pub use oci_spec::image::{Arch, Config};
+use oci_spec::image::{ImageConfiguration, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use sha2::digest::generic_array::GenericArray;
+use sha2::{Digest, Sha256};
+use tar::{Archive, EntryType};
+use thiserror::Error;
+
+mod oci_tar_image;
+use oci_tar_image::{OciTarImage, OciTarImageBlob};
+
+const WHITEOUT_PREFIX: &str = ".wh.";
+const OPAQUE_WHITEOUT_NAME: &str = ".wh..wh..opq";
+
+fn compute_digest<R: Read, H: Digest>(
+ mut reader: R,
+ mut hasher: H,
+) -> std::io::Result<GenericArray<u8, H::OutputSize>> {
+ let mut buf = proxmox_io::boxed::zeroed(32768);
+
+ loop {
+ let bytes_read = reader.read(&mut buf)?;
+ if bytes_read == 0 {
+ break Ok(hasher.finalize());
+ }
+
+ hasher.update(&buf[..bytes_read]);
+ }
+}
+
+fn compute_sha256<R: Read>(reader: R) -> std::io::Result<oci_spec::image::Sha256Digest> {
+ let digest = compute_digest(reader, Sha256::new())?;
+ Ok(oci_spec::image::Sha256Digest::from_str(&format!("{digest:x}")).expect("valid digest"))
+}
+
+/// Build a mapping from uncompressed layer digests (as found in the image config's `rootfs.diff_ids`)
+/// to their corresponding compressed-layer digests (i.e. the filenames under `blobs/<algorithm>/<digest>`)
+fn build_layer_map<R: Read + Seek>(
+ mut oci_tar_image: OciTarImage<R>,
+ image_manifest: &ImageManifest,
+) -> Result<
+ (
+ OciTarImage<R>,
+ HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor>,
+ ),
+ ExtractError,
+> {
+ let mut layer_mapping = HashMap::new();
+
+ for layer in image_manifest.layers() {
+ let digest = match layer.media_type() {
+ MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => layer.digest().clone(),
+ MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
+ let mut compressed_blob = oci_tar_image
+ .open_blob(layer.digest())
+ .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
+ let decoder = GzDecoder::new(&mut compressed_blob);
+ let hash = compute_sha256(decoder)?.into();
+ oci_tar_image = compressed_blob.into_oci_tar_image();
+ hash
+ }
+ MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
+ let mut compressed_blob = oci_tar_image
+ .open_blob(layer.digest())
+ .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
+ let decoder = zstd::Decoder::new(&mut compressed_blob)?;
+ let hash = compute_sha256(decoder)?.into();
+ oci_tar_image = compressed_blob.into_oci_tar_image();
+ hash
+ }
+ // Skip any other non-ImageLayer related media types.
+ // Match explicitly to avoid missing new image layer types when oci-spec updates.
+ MediaType::Descriptor
+ | MediaType::LayoutHeader
+ | MediaType::ImageManifest
+ | MediaType::ImageIndex
+ | MediaType::ImageConfig
+ | MediaType::ArtifactManifest
+ | MediaType::EmptyJSON
+ | MediaType::Other(_) => continue,
+ };
+
+ layer_mapping.insert(digest, layer.clone());
+ }
+
+ Ok((oci_tar_image, layer_mapping))
+}
+
+#[derive(Debug, Error)]
+pub enum ProxmoxOciError {
+ #[error("Error while parsing OCI image: {0}")]
+ ParseError(#[from] ParseError),
+ #[error("Error while extracting OCI image: {0}")]
+ ExtractError(#[from] ExtractError),
+}
+
+/// Extract the rootfs of an OCI image tar and return the image config.
+///
+/// # Arguments
+///
+/// * `oci_tar_path` - Path to the OCI image tar archive
+/// * `rootfs_path` - Destination path where the rootfs will be extracted to
+/// * `arch` - Optional CPU architecture used to pick the first matching manifest from a multi-arch
+/// image index. If `None`, the first manifest will be used.
+pub fn parse_and_extract_image<P: AsRef<Path>>(
+ oci_tar_path: P,
+ rootfs_path: P,
+ arch: Option<&Arch>,
+) -> Result<Option<Config>, ProxmoxOciError> {
+ let (oci_tar_image, image_manifest, image_config) = parse_image(oci_tar_path, arch)?;
+
+ extract_image_rootfs(oci_tar_image, &image_manifest, &image_config, rootfs_path)?;
+
+ Ok(image_config.config().clone())
+}
+
+#[derive(Debug, Error)]
+pub enum ParseError {
+ #[error("OCI spec error: {0}")]
+ OciSpec(#[from] OciSpecError),
+ #[error("Wrong media type")]
+ WrongMediaType,
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("Unsupported CPU architecture")]
+ UnsupportedArchitecture,
+ #[error("Missing image config")]
+ MissingImageConfig,
+}
+
+fn parse_image<P: AsRef<Path>>(
+ oci_tar_path: P,
+ arch: Option<&Arch>,
+) -> Result<(OciTarImage<File>, ImageManifest, ImageConfiguration), ParseError> {
+ let oci_tar_file = File::open(oci_tar_path)?;
+ let mut oci_tar_image = OciTarImage::new(oci_tar_file)?;
+
+ let image_manifest = oci_tar_image
+ .image_manifest(arch)
+ .ok_or(ParseError::UnsupportedArchitecture)??;
+
+ let image_config_descriptor = image_manifest.config();
+
+ if image_config_descriptor.media_type() != &MediaType::ImageConfig {
+ return Err(ParseError::WrongMediaType);
+ }
+
+ let mut image_config_file = oci_tar_image
+ .open_blob(image_config_descriptor.digest())
+ .ok_or(ParseError::MissingImageConfig)?;
+ let image_config = ImageConfiguration::from_reader(&mut image_config_file)?;
+
+ Ok((
+ image_config_file.into_oci_tar_image(),
+ image_manifest,
+ image_config,
+ ))
+}
+
+#[derive(Debug, Error)]
+pub enum ExtractError {
+ #[error("Incorrectly formatted digest: \"{0}\"")]
+ InvalidDigest(String),
+ #[error("Unknown layer digest {0} found in rootfs.diff_ids")]
+ UnknownLayerDigest(oci_spec::image::Digest),
+ #[error("Layer file {0} mentioned in image manifest is missing")]
+ MissingLayerFile(oci_spec::image::Digest),
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("Layer has wrong media type: {0}")]
+ WrongMediaType(String),
+}
+
+fn extract_image_rootfs<R: Read + Seek, P: AsRef<Path>>(
+ oci_tar_image: OciTarImage<R>,
+ image_manifest: &ImageManifest,
+ image_config: &ImageConfiguration,
+ target_path: P,
+) -> Result<(), ExtractError> {
+ let (mut oci_tar_image, layer_map) = build_layer_map(oci_tar_image, image_manifest)?;
+
+ for layer in image_config.rootfs().diff_ids() {
+ let layer_digest = oci_spec::image::Digest::from_str(layer)
+ .map_err(|_| ExtractError::InvalidDigest(layer.to_string()))?;
+ let layer_descriptor = layer_map
+ .get(&layer_digest)
+ .ok_or(ExtractError::UnknownLayerDigest(layer_digest.clone()))?;
+ let mut layer_file = oci_tar_image
+ .open_blob(layer_descriptor.digest())
+ .ok_or(ExtractError::MissingLayerFile(layer_digest))?;
+
+ type DecodeFn<T> = Box<dyn for<'a> Fn(&'a mut T) -> std::io::Result<Box<dyn Read + 'a>>>;
+ let decode_fn: DecodeFn<OciTarImageBlob<R>> = match layer_descriptor.media_type() {
+ MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => {
+ Box::new(|file| Ok(Box::new(file)))
+ }
+ MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
+ Box::new(|file| Ok(Box::new(GzDecoder::new(file))))
+ }
+ MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
+ Box::new(|file| Ok(Box::new(zstd::Decoder::new(file)?)))
+ }
+ // Error on any other non-ImageLayer related media types.
+ // Match explicitly to avoid missing new image layer types when oci-spec updates.
+ media_type @ (MediaType::Descriptor
+ | MediaType::LayoutHeader
+ | MediaType::ImageManifest
+ | MediaType::ImageIndex
+ | MediaType::ImageConfig
+ | MediaType::ArtifactManifest
+ | MediaType::EmptyJSON
+ | MediaType::Other(_)) => {
+ return Err(ExtractError::WrongMediaType(media_type.to_string()))
+ }
+ };
+
+ apply_whiteouts(&mut decode_fn(&mut layer_file)?, &target_path)?;
+ layer_file.seek(SeekFrom::Start(0))?;
+ extract_archive(&mut decode_fn(&mut layer_file)?, &target_path)?;
+
+ oci_tar_image = layer_file.into_oci_tar_image();
+ }
+
+ Ok(())
+}
+
+/// Apply whiteouts on previous layers
+fn apply_whiteouts<R: Read, P: AsRef<Path>>(reader: &mut R, target_path: P) -> std::io::Result<()> {
+ let mut archive = Archive::new(reader);
+
+ for entry in archive.entries()? {
+ let file = entry?;
+ if file.header().entry_type() != EntryType::Regular {
+ continue;
+ }
+
+ let filepath = file.path()?;
+ if let Some(filename) = filepath.file_name() {
+ if filename == OPAQUE_WHITEOUT_NAME {
+ if let Some(parent) = filepath.parent() {
+ let whiteout_abs_path = target_path.as_ref().join(parent);
+ if whiteout_abs_path.exists() {
+ for direntry in read_dir(whiteout_abs_path)? {
+ remove_path(direntry?.path())?;
+ }
+ }
+ }
+ } else if let Some(filename) = filename.to_str() {
+ // TODO: Simplify this once OsStr::strip_prefix is implemented
+ if let Some(filename_stripped) = filename.strip_prefix(WHITEOUT_PREFIX) {
+ let whiteout_path = match filename_stripped {
+ "." => match filepath.parent() {
+ Some(p) if p.parent().is_some() => p,
+ _ => continue, // Prevent whiteout of root directory
+ },
+ ".." => continue, // Prevent whiteout of grandparent directory
+ fname => &filepath.with_file_name(fname),
+ };
+ let whiteout_abs_path = target_path.as_ref().join(whiteout_path);
+ if whiteout_abs_path.exists() {
+ remove_path(whiteout_abs_path)?;
+ }
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn extract_archive<R: Read, P: AsRef<Path>>(reader: &mut R, target_path: P) -> std::io::Result<()> {
+ let mut archive = Archive::new(reader);
+ archive.set_preserve_ownerships(true);
+ archive.set_preserve_permissions(true);
+ archive.set_unpack_xattrs(true);
+
+ // Delay directory entries until the end (they will be created if needed by descendants),
+ // to ensure that directory permissions do not interfere with descendant extraction.
+ let mut directories = Vec::new();
+ for entry in archive.entries()? {
+ let mut file = entry?;
+ if file.header().entry_type() == EntryType::Directory {
+ directories.push(file);
+ continue;
+ } else if file.header().entry_type() == EntryType::Regular {
+ // Skip whiteout files
+ if let Some(filename) = file.path()?.file_name() {
+ if filename == OPAQUE_WHITEOUT_NAME {
+ continue;
+ } else if let Some(filename) = filename.to_str() {
+ if filename.starts_with(WHITEOUT_PREFIX) {
+ continue;
+ }
+ }
+ }
+ }
+
+ file.unpack_in(&target_path)?;
+ }
+
+ // Apply the directories in reverse topological order,
+ // to avoid failure on restrictive parent directory permissions.
+ directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes()));
+ for mut dir in directories {
+ dir.unpack_in(&target_path)?;
+ }
+
+ Ok(())
+}
+
+fn remove_path(path: PathBuf) -> std::io::Result<()> {
+ if path.metadata()?.is_dir() {
+ remove_dir_all(path)
+ } else {
+ remove_file(path)
+ }
+}
diff --git a/proxmox-oci/src/oci_tar_image.rs b/proxmox-oci/src/oci_tar_image.rs
new file mode 100644
index 00000000..23e1bfe0
--- /dev/null
+++ b/proxmox-oci/src/oci_tar_image.rs
@@ -0,0 +1,144 @@
+use std::collections::HashMap;
+use std::io::{Read, Seek, SeekFrom};
+use std::ops::Range;
+use std::path::{Path, PathBuf};
+
+use oci_spec::image::{Arch, Digest, ImageIndex, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use tar::Archive;
+
+use proxmox_io::RangeReader;
+
+#[derive(Clone)]
+struct TarEntry {
+ range: Range<u64>,
+}
+
+impl TarEntry {
+ fn new(range: Range<u64>) -> Self {
+ Self { range }
+ }
+}
+
+pub struct OciTarImage<R: Read + Seek> {
+ reader: R,
+ entries: HashMap<PathBuf, TarEntry>,
+ image_index: ImageIndex,
+}
+
+impl<R: Read + Seek> OciTarImage<R> {
+ pub fn new(reader: R) -> oci_spec::Result<Self> {
+ let mut archive = Archive::new(reader);
+ let entries = archive.entries_with_seek()?;
+ let mut entries_index = HashMap::new();
+ let mut image_index = None;
+
+ for entry in entries {
+ let mut entry = entry?;
+ let offset = entry.raw_file_position();
+ let size = entry.size();
+ let path = entry.path()?.into_owned();
+
+ if path.as_path() == Path::new("index.json") {
+ image_index = Some(ImageIndex::from_reader(&mut entry)?);
+ }
+
+ let tar_entry = TarEntry::new(offset..(offset + size));
+ entries_index.insert(path, tar_entry);
+ }
+
+ if let Some(image_index) = image_index {
+ Ok(Self {
+ reader: archive.into_inner(),
+ entries: entries_index,
+ image_index,
+ })
+ } else {
+ Err(OciSpecError::Other("Missing index.json file".into()))
+ }
+ }
+
+ pub fn image_index(&self) -> &ImageIndex {
+ &self.image_index
+ }
+
+ fn get_blob_entry(&self, digest: &Digest) -> Option<TarEntry> {
+ let path = get_blob_path(digest);
+ self.entries.get(&path).cloned()
+ }
+
+ pub fn open_blob(self, digest: &Digest) -> Option<OciTarImageBlob<R>> {
+ if let Some(entry) = self.get_blob_entry(digest) {
+ Some(OciTarImageBlob::new(self, entry.range))
+ } else {
+ None
+ }
+ }
+
+ pub fn image_manifest(
+ &mut self,
+ architecture: Option<&Arch>,
+ ) -> Option<oci_spec::Result<ImageManifest>> {
+ let digest = match self.image_index.manifests().iter().find(|d| {
+ d.media_type() == &MediaType::ImageManifest
+ && architecture
+ .is_none_or(|a| d.platform().as_ref().is_none_or(|p| p.architecture() == a))
+ }) {
+ Some(descriptor) => descriptor.digest(),
+ None => return None,
+ };
+
+ if let Some(entry) = self.get_blob_entry(digest) {
+ let mut range_reader = RangeReader::new(&mut self.reader, entry.range);
+ Some(ImageManifest::from_reader(&mut range_reader))
+ } else {
+ Some(Err(OciSpecError::Other(format!(
+ "Image manifest with digest {digest} mentioned in image index is missing"
+ ))))
+ }
+ }
+}
+
+fn get_blob_path(digest: &Digest) -> PathBuf {
+ let algorithm = digest.algorithm();
+ let digest = digest.digest();
+ format!("blobs/{algorithm}/{digest}").into()
+}
+
+pub struct OciTarImageBlob<R: Read + Seek> {
+ range_reader: RangeReader<R>,
+ entries: HashMap<PathBuf, TarEntry>,
+ image_index: ImageIndex,
+}
+
+impl<R: Read + Seek> OciTarImageBlob<R> {
+ fn new(archive: OciTarImage<R>, range: Range<u64>) -> Self {
+ let range_reader = RangeReader::new(archive.reader, range);
+
+ Self {
+ range_reader,
+ entries: archive.entries,
+ image_index: archive.image_index,
+ }
+ }
+
+ pub fn into_oci_tar_image(self) -> OciTarImage<R> {
+ OciTarImage {
+ reader: self.range_reader.into_inner(),
+ entries: self.entries,
+ image_index: self.image_index,
+ }
+ }
+}
+
+impl<R: Read + Seek> Read for OciTarImageBlob<R> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.range_reader.read(buf)
+ }
+}
+
+impl<R: Read + Seek> Seek for OciTarImageBlob<R> {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+ self.range_reader.seek(pos)
+ }
+}
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2025-09-08 15:04 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-09-08 15:02 [pve-devel] [PATCH container/docs/lxc/manager/proxmox{, -perl-rs}/storage v4 00/15] support OCI images as container templates Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH proxmox v4 01/15] io: introduce RangeReader for bounded reads Filip Schauer
2025-09-08 15:02 ` Filip Schauer [this message]
2025-09-08 15:02 ` [pve-devel] [PATCH proxmox v4 03/15] proxmox-oci: add tests for whiteout handling Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH proxmox-perl-rs v4 04/15] add Perl mapping for OCI container image parser/extractor Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH lxc v4 05/15] lxc: conf: split `lxc.environment` into `runtime` and `hooks` Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 06/15] config: add `lxc.environment.runtime`/`hooks` Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 07/15] add support for OCI images as container templates Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 08/15] config: add entrypoint parameter Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 09/15] configure static IP in LXC config for custom entrypoint Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 10/15] setup: debian: create /etc/network path if missing Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 11/15] setup: recursively mkdir /etc/systemd/{network, system-preset} Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 12/15] implement host-managed DHCP for containers with `ipmanagehost` Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH storage v4 13/15] allow .tar container templates Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH manager v4 14/15] ui: storage upload: accept *.tar files as vztmpl Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH docs v4 15/15] ct: add OCI image docs Filip Schauer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250908150224.155373-3-f.schauer@proxmox.com \
--to=f.schauer@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.