public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Christoph Heiss" <c.heiss@proxmox.com>
To: "Filip Schauer" <f.schauer@proxmox.com>
Cc: Proxmox VE development discussion <pve-devel@lists.proxmox.com>
Subject: Re: [pve-devel] [PATCH proxmox 1/9] add proxmox-oci crate
Date: Mon, 02 Jun 2025 11:33:19 +0200	[thread overview]
Message-ID: <DABXZUAQK7EF.1W83RGAP8S4VI@proxmox.com> (raw)
In-Reply-To: <20250520124257.165949-2-f.schauer@proxmox.com>

Some comments inline.

On Tue May 20, 2025 at 2:42 PM CEST, Filip Schauer wrote:
> [..]
> diff --git a/proxmox-oci/src/lib.rs b/proxmox-oci/src/lib.rs
> new file mode 100644
> index 00000000..57bd48b4
> --- /dev/null
> +++ b/proxmox-oci/src/lib.rs
> @@ -0,0 +1,165 @@
> [..]
> +/// 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>(
> +    oci_tar_image: &mut OciTarImage<R>,
> +    image_manifest: &ImageManifest,
> +) -> HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor> {
> +    let mut layer_mapping = HashMap::new();
> +
> +    for layer in image_manifest.layers() {
> +        match layer.media_type() {
> +            MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => {
> +                layer_mapping.insert(layer.digest().clone(), layer.clone());
> +            }
> +            MediaType::ImageLayerGzip
> +            | MediaType::ImageLayerNonDistributableGzip
> +            | MediaType::ImageLayerZstd
> +            | MediaType::ImageLayerNonDistributableZstd => {
> +                let compressed_blob = oci_tar_image.open_blob(layer.digest()).unwrap();
> +                let mut decoder: Box<dyn Read> = match layer.media_type() {
> +                    MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> +                        Box::new(GzDecoder::new(compressed_blob))
> +                    }
> +                    MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> +                        Box::new(zstd::Decoder::new(compressed_blob).unwrap())
> +                    }
> +                    _ => unreachable!(),
> +                };
> +                let mut hasher = Sha256::new();
> +                let mut buf = proxmox_io::boxed::zeroed(4096);
> +
> +                loop {
> +                    let bytes_read = decoder.read(&mut buf).unwrap();
> +                    if bytes_read == 0 {
> +                        break;
> +                    }
> +
> +                    hasher.update(&buf[..bytes_read]);
> +                }
> +
> +                let uncompressed_digest =
> +                    oci_spec::image::Sha256Digest::from_str(&format!("{:x}", hasher.finalize()))
> +                        .unwrap();

For this variant, the hashing part could be extracted into a separate
function and then the Gzip/Zstd can be proper independent match arms.
E.g. each having the decoder construction, calling the layer hashing
function and then `.insert()` mapping below.

Has the nice side-effect of avoiding things like the `unreachable!()`
above, too.

> +
> +                layer_mapping.insert(uncompressed_digest.into(), layer.clone());
> +            }
> +            _ => (),
> +        }
> +    }
> +
> +    layer_mapping
> +}
> +
> +pub enum ProxmoxOciError {
> +    NotAnOciImage,
> +    OciSpecError(OciSpecError),
> +}

Since this is only used by `parse_oci_image()`, rename it to something
like ParseError and/or ExtractError, depending on the below.

> +
> +impl From<OciSpecError> for ProxmoxOciError {
> +    fn from(oci_spec_err: OciSpecError) -> Self {
> +        Self::OciSpecError(oci_spec_err)
> +    }
> +}
> +
> +impl From<std::io::Error> for ProxmoxOciError {
> +    fn from(io_err: std::io::Error) -> Self {
> +        Self::OciSpecError(io_err.into())
> +    }
> +}
> +
> +pub fn parse_oci_image(

Seeing as this function not only parses, but also extract the contents
at the same time, this should be named something like
`parse_and_extract_image()`?

(The `_oci` is implied through the crate name already anyway.)

> +    oci_tar_path: &str,
> +    rootfs_path: &str,
> +) -> Result<Option<Config>, ProxmoxOciError> {
> +    let oci_tar_file = File::open(oci_tar_path)?;
> +    let mut oci_tar_image = match OciTarImage::new(oci_tar_file) {
> +        Ok(oci_tar_image) => oci_tar_image,
> +        Err(_) => return Err(ProxmoxOciError::NotAnOciImage),

I'd also separate the `OciSpecError::Io(_)` variant here, possible into
a separate `ProxmoxOciError::IoError` variant. Callers might want to
handle I/O errors other than e.g. actual parsing errors.

> +    };
> +    let image_manifest = oci_tar_image.image_manifest(&Arch::Amd64)?;
> +
> +    let image_config_descriptor = image_manifest.config();
> +
> +    if image_config_descriptor.media_type() != &MediaType::ImageConfig {
> +        return Err(ProxmoxOciError::OciSpecError(OciSpecError::Other(
> +            "ImageConfig has wrong media type".into(),
> +        )));

Can simply be separate variant, e.g. `ProxmoxOciError::WrongMediaType`

> +    }
> +
> +    let image_config_file = oci_tar_image.open_blob(image_config_descriptor.digest())?;
> +    let image_config = ImageConfiguration::from_reader(image_config_file)?;
> +
> +    extract_oci_image_rootfs(
> +        &mut oci_tar_image,
> +        &image_manifest,
> +        &image_config,
> +        rootfs_path.into(),
> +    )?;
> +
> +    Ok(image_config.config().clone())
> +}
> +
> +fn extract_oci_image_rootfs<R: Read + Seek>(
> +    oci_tar_image: &mut OciTarImage<R>,
> +    image_manifest: &ImageManifest,
> +    image_config: &ImageConfiguration,
> +    target_path: PathBuf,
> +) -> oci_spec::Result<()> {
> +    if !target_path.exists() {
> +        return Err(OciSpecError::Other(
> +            "Rootfs destination path not found".into(),
> +        ));
> +    }
> +
> +    let 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)?;
> +
> +        let layer_descriptor = match layer_map.get(&layer_digest) {
> +            Some(descriptor) => descriptor,
> +            None => {
> +                return Err(OciSpecError::Other(
> +                    "Unknown layer digest found in rootfs.diff_ids".into(),
> +                ));
> +            }
> +        };
> +
> +        let layer_file = oci_tar_image.open_blob(layer_descriptor.digest())?;
> +
> +        let tar_file: Box<dyn Read> = match layer_descriptor.media_type() {
> +            MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => Box::new(layer_file),
> +            MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> +                Box::new(GzDecoder::new(layer_file))
> +            }
> +            MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> +                Box::new(zstd::Decoder::new(layer_file)?)
> +            }
> +            _ => {
> +                return Err(OciSpecError::Other(
> +                    "Encountered invalid media type for rootfs layer".into(),
> +                ));

Again I'd just introduce a separate error variant or even re-use
`ProxmoxOciError::WrongMediaType`, seems appropriate too.

> +            }
> +        };
> +
> +        let mut archive = Archive::new(tar_file);
> +        archive.set_preserve_ownerships(true);
> +        archive.unpack(&target_path)?;
> +    }
> +
> +    Ok(())
> +}
> diff --git a/proxmox-oci/src/oci_tar_image.rs b/proxmox-oci/src/oci_tar_image.rs
> new file mode 100644
> index 00000000..02199c75
> --- /dev/null
> +++ b/proxmox-oci/src/oci_tar_image.rs
> @@ -0,0 +1,173 @@
> [..]
> +pub struct OciTarImage<R: Read + Seek> {
> +    archive: TarArchive<R>,
> +    image_index: ImageIndex,
> +}
> +
> +impl<R: Read + Seek> OciTarImage<R> {
> [..]
> +    pub fn image_manifest(&mut self, architecture: &Arch) -> oci_spec::Result<ImageManifest> {
> +        let image_manifest_descriptor = match self.image_index.manifests().iter().find(|&x| {
> +            x.media_type() == &MediaType::ImageManifest
> +                && x.platform()
> +                    .as_ref()
> +                    .is_none_or(|platform| platform.architecture() == architecture)
> +        }) {
> +            Some(descriptor) => descriptor,
> +            None => {
> +                return Err(OciSpecError::Io(std::io::Error::new(
> +                    std::io::ErrorKind::NotFound,
> +                    "Image manifest not found for architecture",
> +                )));

Doesn't really seem like an I/O error?

> +            }
> +        };
> +
> +        let image_manifest_file = self.open_blob(&image_manifest_descriptor.digest().clone())?;
> +
> +        ImageManifest::from_reader(image_manifest_file)
> +    }
> +}
> +
> +fn get_blob_path(digest: &Digest) -> PathBuf {
> +    let algorithm = digest.algorithm().as_ref();
> +    let digest = digest.digest();
> +    PathBuf::from(format!("blobs/{algorithm}/{digest}"))

The path can also be constructed using the primivites from `PathBuf`
itself, e.g.

  let mut path = PathBuf::new();
  path.push(digest.algorithm().as_ref());
  ..

> +}



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


  reply	other threads:[~2025-06-02  9:33 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-05-20 12:42 [pve-devel] [PATCH container/proxmox{, -perl-rs}/storage 0/9] support OCI images as container templates Filip Schauer
2025-05-20 12:42 ` [pve-devel] [PATCH proxmox 1/9] add proxmox-oci crate Filip Schauer
2025-06-02  9:33   ` Christoph Heiss [this message]
2025-05-20 12:42 ` [pve-devel] [PATCH proxmox-perl-rs 2/9] add Perl mapping for OCI container image parser Filip Schauer
2025-06-02  9:34   ` Christoph Heiss
2025-05-20 12:42 ` [pve-devel] [PATCH storage 3/9] allow .tar container templates Filip Schauer
2025-06-02 14:16   ` Michael Köppl
2025-05-20 12:42 ` [pve-devel] [PATCH container 4/9] config: whitelist lxc.init.cwd Filip Schauer
2025-05-20 12:42 ` [pve-devel] [PATCH container 5/9] add support for OCI images as container templates Filip Schauer
2025-05-20 12:42 ` [pve-devel] [PATCH container 6/9] config: add entrypoint parameter Filip Schauer
2025-05-20 12:42 ` [pve-devel] [PATCH container 7/9] configure static IP in LXC config for custom entrypoint Filip Schauer
2025-05-20 12:42 ` [pve-devel] [PATCH container 8/9] setup: debian: create /etc/network path if missing Filip Schauer
2025-06-02  9:37   ` Christoph Heiss
2025-06-02 10:49     ` Filip Schauer
2025-05-20 12:42 ` [pve-devel] [PATCH container 9/9] manage DHCP for containers with custom entrypoint Filip Schauer
2025-06-02 16:26 ` [pve-devel] [PATCH container/proxmox{, -perl-rs}/storage 0/9] support OCI images as container templates Michael Köppl
2025-06-11 15:02   ` Filip Schauer
2025-06-06 13:19 ` Christoph Heiss
2025-06-11 15:09   ` Filip Schauer
2025-06-11 14:55 ` [pve-devel] superseded: " 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=DABXZUAQK7EF.1W83RGAP8S4VI@proxmox.com \
    --to=c.heiss@proxmox.com \
    --cc=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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal