Skip to main content

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