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