ostree_ext/container/
update_detachedmeta.rs

1use super::ImageReference;
2use crate::container::{DIFFID_LABEL, skopeo};
3use crate::container::{Transport, store as container_store};
4use anyhow::{Context, Result, anyhow};
5use camino::Utf8Path;
6use cap_std::fs::Dir;
7use cap_std_ext::cap_std;
8use containers_image_proxy::oci_spec::image as oci_image;
9use std::io::{BufReader, BufWriter};
10
11/// Given an OSTree container image reference, update the detached metadata (e.g. GPG signature)
12/// while preserving all other container image metadata.
13///
14/// The return value is the manifest digest of (e.g. `@sha256:`) the image.
15pub async fn update_detached_metadata(
16    src: &ImageReference,
17    dest: &ImageReference,
18    detached_buf: Option<&[u8]>,
19) -> Result<oci_image::Digest> {
20    // For now, convert the source to a temporary OCI directory, so we can directly
21    // parse and manipulate it.  In the future this will be replaced by https://github.com/ostreedev/ostree-rs-ext/issues/153
22    // and other work to directly use the containers/image API via containers-image-proxy.
23    let tempdir = tempfile::tempdir_in("/var/tmp")?;
24    let tempsrc = tempdir.path().join("src");
25    let tempsrc_utf8 = Utf8Path::from_path(&tempsrc).ok_or_else(|| anyhow!("Invalid tempdir"))?;
26    let tempsrc_ref = ImageReference {
27        transport: Transport::OciDir,
28        name: tempsrc_utf8.to_string(),
29    };
30
31    // Full copy of the source image
32    let pulled_digest = skopeo::copy(src, &tempsrc_ref, None, None, false)
33        .await
34        .context("Creating temporary copy to OCI dir")?;
35
36    // Copy to the thread
37    let detached_buf = detached_buf.map(Vec::from);
38    let tempsrc_ref_path = tempsrc_ref.name.clone();
39    // Fork a thread to do the heavy lifting of filtering the tar stream, rewriting the manifest/config.
40    crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
41        // Open the temporary OCI directory.
42        let tempsrc = Dir::open_ambient_dir(tempsrc_ref_path, cap_std::ambient_authority())
43            .context("Opening src")?;
44        let tempsrc = ocidir::OciDir::open(tempsrc)?;
45
46        // Load the manifest, platform, and config
47        let idx = tempsrc.read_index()?;
48        let manifest_descriptor = idx
49            .manifests()
50            .first()
51            .ok_or(anyhow!("No manifests in index"))?;
52        let mut manifest: oci_image::ImageManifest = tempsrc
53            .read_json_blob(manifest_descriptor)
54            .context("Reading manifest json blob")?;
55
56        anyhow::ensure!(manifest_descriptor.digest() == &pulled_digest);
57        let platform = manifest_descriptor
58            .platform()
59            .as_ref()
60            .cloned()
61            .unwrap_or_default();
62        let mut config: oci_image::ImageConfiguration =
63            tempsrc.read_json_blob(manifest.config())?;
64        let mut ctrcfg = config
65            .config()
66            .as_ref()
67            .cloned()
68            .ok_or_else(|| anyhow!("Image is missing container configuration"))?;
69
70        // Find the OSTree commit layer we want to replace
71        let (commit_layer, _, _) =
72            container_store::parse_ostree_manifest_layout(&manifest, &config)?;
73        let commit_layer_idx = manifest
74            .layers()
75            .iter()
76            .position(|x| x == commit_layer)
77            .unwrap();
78
79        // Create a new layer
80        let out_layer = {
81            // Create tar streams for source and destination
82            let src_layer = BufReader::new(tempsrc.read_blob(commit_layer)?);
83            let mut src_layer = flate2::read::GzDecoder::new(src_layer);
84            let mut out_layer = BufWriter::new(tempsrc.create_gzip_layer(None)?);
85
86            // Process the tar stream and inject our new detached metadata
87            crate::tar::update_detached_metadata(
88                &mut src_layer,
89                &mut out_layer,
90                detached_buf.as_deref(),
91                Some(cancellable),
92            )?;
93
94            // Flush all wrappers, and finalize the layer
95            out_layer
96                .into_inner()
97                .map_err(|_| anyhow!("Failed to flush buffer"))?
98                .complete()?
99        };
100        // Get the diffid and descriptor for our new tar layer
101        let out_layer_diffid = format!("sha256:{}", out_layer.uncompressed_sha256.digest());
102        let out_layer_descriptor = out_layer
103            .descriptor()
104            .media_type(oci_image::MediaType::ImageLayerGzip)
105            .build()
106            .unwrap(); // SAFETY: We pass all required fields
107
108        // Splice it into both the manifest and config
109        manifest.layers_mut()[commit_layer_idx] = out_layer_descriptor;
110        config.rootfs_mut().diff_ids_mut()[commit_layer_idx].clone_from(&out_layer_diffid);
111
112        let labels = ctrcfg.labels_mut().get_or_insert_with(Default::default);
113        // Nothing to do except in the special case where there's somehow only one
114        // chunked layer.
115        if manifest.layers().len() == 1 {
116            labels.insert(DIFFID_LABEL.into(), out_layer_diffid);
117        }
118        config.set_config(Some(ctrcfg));
119
120        // Write the config and manifest
121        let new_config_descriptor = tempsrc.write_config(config)?;
122        manifest.set_config(new_config_descriptor);
123        // This entirely replaces the single entry in the OCI directory, which skopeo will find by default.
124        tempsrc
125            .replace_with_single_manifest(manifest, platform)
126            .context("Writing manifest")?;
127        Ok(())
128    })
129    .await
130    .context("Regenerating commit layer")?;
131
132    // Finally, copy the mutated image back to the target.  For chunked images,
133    // because we only changed one layer, skopeo should know not to re-upload shared blobs.
134    crate::container::skopeo::copy(&tempsrc_ref, dest, None, None, false)
135        .await
136        .context("Copying to destination")
137}