bootc_lib/bootc_composefs/
export.rs

1use std::{fs::File, os::fd::AsRawFd};
2
3use anyhow::{Context, Result};
4use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
5use composefs::splitstream::SplitStreamData;
6use ocidir::{oci_spec::image::Platform, OciDir};
7use ostree_ext::container::skopeo;
8use ostree_ext::{container::Transport, oci_spec::image::ImageConfiguration};
9use tar::EntryType;
10
11use crate::image::get_imgrefs_for_copy;
12use crate::{
13    bootc_composefs::{
14        status::{get_composefs_status, get_imginfo},
15        update::str_to_sha256digest,
16    },
17    store::{BootedComposefs, Storage},
18};
19
20/// Exports a composefs repository to a container image in containers-storage:
21pub async fn export_repo_to_image(
22    storage: &Storage,
23    booted_cfs: &BootedComposefs,
24    source: Option<&str>,
25    target: Option<&str>,
26) -> Result<()> {
27    let host = get_composefs_status(storage, booted_cfs).await?;
28
29    let (source, dest_imgref) = get_imgrefs_for_copy(&host, source, target).await?;
30
31    let mut depl_verity = None;
32
33    for depl in host
34        .status
35        .booted
36        .iter()
37        .chain(host.status.staged.iter())
38        .chain(host.status.rollback.iter())
39        .chain(host.status.other_deployments.iter())
40    {
41        let img = &depl.image.as_ref().unwrap().image;
42
43        // Not checking transport here as we'll be pulling from the repo anyway
44        // So, image name is all we need
45        if img.image == source.name {
46            depl_verity = Some(depl.require_composefs()?.verity.clone());
47            break;
48        }
49    }
50
51    let depl_verity = depl_verity.ok_or_else(|| anyhow::anyhow!("Image {source} not found"))?;
52
53    let imginfo = get_imginfo(storage, &depl_verity, None).await?;
54
55    let config_name = &imginfo.manifest.config().digest().digest();
56    let config_name = str_to_sha256digest(config_name)?;
57
58    let var_tmp =
59        Dir::open_ambient_dir("/var/tmp", ambient_authority()).context("Opening /var/tmp")?;
60
61    let tmpdir = cap_std_ext::cap_tempfile::tempdir_in(&var_tmp)?;
62    let oci_dir = OciDir::ensure(tmpdir.try_clone()?).context("Opening OCI")?;
63
64    let mut config_stream = booted_cfs
65        .repo
66        .open_stream(&hex::encode(config_name), None)
67        .context("Opening config stream")?;
68
69    let config = ImageConfiguration::from_reader(&mut config_stream)?;
70
71    // We can't guarantee that we'll get the same tar stream as the container image
72    // So we create new config and manifest
73    let mut new_config = config.clone();
74    if let Some(history) = new_config.history_mut() {
75        history.clear();
76    }
77    new_config.rootfs_mut().diff_ids_mut().clear();
78
79    let mut new_manifest = imginfo.manifest.clone();
80    new_manifest.layers_mut().clear();
81
82    let total_layers = config.rootfs().diff_ids().len();
83
84    for (idx, old_diff_id) in config.rootfs().diff_ids().iter().enumerate() {
85        let layer_sha256 = str_to_sha256digest(old_diff_id)?;
86        let layer_verity = config_stream.lookup(&layer_sha256)?;
87
88        let mut layer_stream = booted_cfs
89            .repo
90            .open_stream(&hex::encode(layer_sha256), Some(layer_verity))?;
91
92        let mut layer_writer = oci_dir.create_layer(None)?;
93        layer_writer.follow_symlinks(false);
94
95        let mut got_zero_block = false;
96
97        loop {
98            let mut buf = [0u8; 512];
99
100            if !layer_stream
101                .read_inline_exact(&mut buf)
102                .context("Reading into buffer")?
103            {
104                break;
105            }
106
107            let all_zeroes = buf.iter().all(|x| *x == 0);
108
109            // EOF for tar
110            if all_zeroes && got_zero_block {
111                break;
112            } else if all_zeroes {
113                got_zero_block = true;
114                continue;
115            }
116
117            got_zero_block = false;
118
119            let header = tar::Header::from_byte_slice(&buf);
120
121            let size = header.entry_size()?;
122
123            match layer_stream.read_exact(size as usize, ((size + 511) & !511) as usize)? {
124                SplitStreamData::External(obj_id) => match header.entry_type() {
125                    EntryType::Regular | EntryType::Continuous => {
126                        let file = File::from(booted_cfs.repo.open_object(&obj_id)?);
127
128                        layer_writer
129                            .append(&header, file)
130                            .context("Failed to write external entry")?;
131                    }
132
133                    _ => anyhow::bail!("Unsupported external-chunked entry {header:?} {obj_id:?}"),
134                },
135
136                SplitStreamData::Inline(content) => match header.entry_type() {
137                    EntryType::Directory => {
138                        layer_writer.append(&header, std::io::empty())?;
139                    }
140
141                    // We do not care what the content is as we're re-archiving it anyway
142                    _ => {
143                        layer_writer
144                            .append(&header, &*content)
145                            .context("Failed to write inline entry")?;
146                    }
147                },
148            };
149        }
150
151        layer_writer.finish()?;
152
153        let layer = layer_writer
154            .into_inner()
155            .context("Getting inner layer writer")?
156            .complete()
157            .context("Writing layer to disk")?;
158
159        tracing::debug!(
160            "Wrote layer: {layer_sha} #{layer_num}/{total_layers}",
161            layer_sha = layer.uncompressed_sha256_as_digest(),
162            layer_num = idx + 1,
163        );
164
165        let previous_annotations = imginfo
166            .manifest
167            .layers()
168            .get(idx)
169            .and_then(|l| l.annotations().as_ref())
170            .cloned();
171
172        let history = imginfo.config.history().as_ref();
173        let history_entry = history.and_then(|v| v.get(idx));
174        let previous_description = history_entry
175            .clone()
176            .and_then(|h| h.comment().as_deref())
177            .unwrap_or_default();
178
179        let previous_created = history_entry
180            .and_then(|h| h.created().as_deref())
181            .and_then(bootc_utils::try_deserialize_timestamp)
182            .unwrap_or_default();
183
184        oci_dir.push_layer_full(
185            &mut new_manifest,
186            &mut new_config,
187            layer,
188            previous_annotations,
189            previous_description,
190            previous_created,
191        );
192    }
193
194    let descriptor = oci_dir.write_config(new_config).context("Writing config")?;
195
196    new_manifest.set_config(descriptor);
197    oci_dir
198        .insert_manifest(new_manifest, None, Platform::default())
199        .context("Writing manifest")?;
200
201    // Pass the temporary oci directory as the current working directory for the skopeo process
202    let tempoci = ostree_ext::container::ImageReference {
203        transport: Transport::OciDir,
204        name: format!("/proc/self/fd/{}", tmpdir.as_raw_fd()),
205    };
206
207    skopeo::copy(
208        &tempoci,
209        &dest_imgref,
210        None,
211        Some((
212            std::sync::Arc::new(tmpdir.try_clone()?.into()),
213            tmpdir.as_raw_fd(),
214        )),
215        true,
216    )
217    .await?;
218
219    Ok(())
220}