-
Notifications
You must be signed in to change notification settings - Fork 166
Implement bootc image copy-to-storage for composefs backend
#1890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
813da0c
composefs: Implement `bootc image copy-to-storage`
Johan-Liebert1 80da5c4
composefs/export: Use `ocidir` for creating OCI compliant directory
Johan-Liebert1 4b7c12d
copy-to-storage: Refactor tar parsing
Johan-Liebert1 c83639d
copy-to-storage: Fix deadlock in ostree path
Johan-Liebert1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| use std::{fs::File, os::fd::AsRawFd}; | ||
|
|
||
| use anyhow::{Context, Result}; | ||
| use cap_std_ext::cap_std::{ambient_authority, fs::Dir}; | ||
| use composefs::splitstream::SplitStreamData; | ||
| use ocidir::{oci_spec::image::Platform, OciDir}; | ||
| use ostree_ext::container::skopeo; | ||
| use ostree_ext::{container::Transport, oci_spec::image::ImageConfiguration}; | ||
| use tar::EntryType; | ||
|
|
||
| use crate::image::get_imgrefs_for_copy; | ||
| use crate::{ | ||
| bootc_composefs::{ | ||
| status::{get_composefs_status, get_imginfo}, | ||
| update::str_to_sha256digest, | ||
| }, | ||
| store::{BootedComposefs, Storage}, | ||
| }; | ||
|
|
||
| /// Exports a composefs repository to a container image in containers-storage: | ||
| pub async fn export_repo_to_image( | ||
| storage: &Storage, | ||
| booted_cfs: &BootedComposefs, | ||
| source: Option<&str>, | ||
| target: Option<&str>, | ||
| ) -> Result<()> { | ||
| let host = get_composefs_status(storage, booted_cfs).await?; | ||
|
|
||
| let (source, dest_imgref) = get_imgrefs_for_copy(&host, source, target).await?; | ||
|
|
||
| let mut depl_verity = None; | ||
|
|
||
| for depl in host | ||
| .status | ||
| .booted | ||
| .iter() | ||
| .chain(host.status.staged.iter()) | ||
| .chain(host.status.rollback.iter()) | ||
| .chain(host.status.other_deployments.iter()) | ||
| { | ||
| let img = &depl.image.as_ref().unwrap().image; | ||
|
|
||
| // Not checking transport here as we'll be pulling from the repo anyway | ||
| // So, image name is all we need | ||
| if img.image == source.name { | ||
| depl_verity = Some(depl.require_composefs()?.verity.clone()); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| let depl_verity = depl_verity.ok_or_else(|| anyhow::anyhow!("Image {source} not found"))?; | ||
|
|
||
| let imginfo = get_imginfo(storage, &depl_verity, None).await?; | ||
|
|
||
| let config_name = &imginfo.manifest.config().digest().digest(); | ||
| let config_name = str_to_sha256digest(config_name)?; | ||
|
|
||
| let var_tmp = | ||
| Dir::open_ambient_dir("/var/tmp", ambient_authority()).context("Opening /var/tmp")?; | ||
|
|
||
| let tmpdir = cap_std_ext::cap_tempfile::tempdir_in(&var_tmp)?; | ||
| let oci_dir = OciDir::ensure(tmpdir.try_clone()?).context("Opening OCI")?; | ||
|
|
||
| let mut config_stream = booted_cfs | ||
| .repo | ||
| .open_stream(&hex::encode(config_name), None) | ||
| .context("Opening config stream")?; | ||
|
|
||
| let config = ImageConfiguration::from_reader(&mut config_stream)?; | ||
|
|
||
| // We can't guarantee that we'll get the same tar stream as the container image | ||
| // So we create new config and manifest | ||
| let mut new_config = config.clone(); | ||
| if let Some(history) = new_config.history_mut() { | ||
| history.clear(); | ||
| } | ||
| new_config.rootfs_mut().diff_ids_mut().clear(); | ||
|
|
||
| let mut new_manifest = imginfo.manifest.clone(); | ||
| new_manifest.layers_mut().clear(); | ||
|
|
||
| let total_layers = config.rootfs().diff_ids().len(); | ||
|
|
||
| for (idx, old_diff_id) in config.rootfs().diff_ids().iter().enumerate() { | ||
| let layer_sha256 = str_to_sha256digest(old_diff_id)?; | ||
| let layer_verity = config_stream.lookup(&layer_sha256)?; | ||
|
|
||
| let mut layer_stream = booted_cfs | ||
| .repo | ||
| .open_stream(&hex::encode(layer_sha256), Some(layer_verity))?; | ||
|
|
||
| let mut layer_writer = oci_dir.create_layer(None)?; | ||
| layer_writer.follow_symlinks(false); | ||
|
|
||
| let mut got_zero_block = false; | ||
|
|
||
| loop { | ||
| let mut buf = [0u8; 512]; | ||
|
|
||
| if !layer_stream | ||
| .read_inline_exact(&mut buf) | ||
| .context("Reading into buffer")? | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| let all_zeroes = buf.iter().all(|x| *x == 0); | ||
|
|
||
| // EOF for tar | ||
| if all_zeroes && got_zero_block { | ||
| break; | ||
| } else if all_zeroes { | ||
| got_zero_block = true; | ||
| continue; | ||
| } | ||
|
|
||
| got_zero_block = false; | ||
|
|
||
| let header = tar::Header::from_byte_slice(&buf); | ||
|
|
||
| let size = header.entry_size()?; | ||
|
|
||
| match layer_stream.read_exact(size as usize, ((size + 511) & !511) as usize)? { | ||
| SplitStreamData::External(obj_id) => match header.entry_type() { | ||
| EntryType::Regular | EntryType::Continuous => { | ||
| let file = File::from(booted_cfs.repo.open_object(&obj_id)?); | ||
|
|
||
| layer_writer | ||
| .append(&header, file) | ||
| .context("Failed to write external entry")?; | ||
| } | ||
|
|
||
| _ => anyhow::bail!("Unsupported external-chunked entry {header:?} {obj_id:?}"), | ||
| }, | ||
|
|
||
| SplitStreamData::Inline(content) => match header.entry_type() { | ||
| EntryType::Directory => { | ||
| layer_writer.append(&header, std::io::empty())?; | ||
| } | ||
|
|
||
| // We do not care what the content is as we're re-archiving it anyway | ||
| _ => { | ||
| layer_writer | ||
| .append(&header, &*content) | ||
| .context("Failed to write inline entry")?; | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| layer_writer.finish()?; | ||
|
|
||
| let layer = layer_writer | ||
| .into_inner() | ||
| .context("Getting inner layer writer")? | ||
| .complete() | ||
| .context("Writing layer to disk")?; | ||
|
|
||
| tracing::debug!( | ||
| "Wrote layer: {layer_sha} #{layer_num}/{total_layers}", | ||
| layer_sha = layer.uncompressed_sha256_as_digest(), | ||
| layer_num = idx + 1, | ||
| ); | ||
|
|
||
| let previous_annotations = imginfo | ||
| .manifest | ||
| .layers() | ||
| .get(idx) | ||
| .and_then(|l| l.annotations().as_ref()) | ||
| .cloned(); | ||
|
|
||
| let history = imginfo.config.history().as_ref(); | ||
| let history_entry = history.and_then(|v| v.get(idx)); | ||
| let previous_description = history_entry | ||
| .clone() | ||
| .and_then(|h| h.comment().as_deref()) | ||
| .unwrap_or_default(); | ||
|
|
||
| let previous_created = history_entry | ||
| .and_then(|h| h.created().as_deref()) | ||
| .and_then(bootc_utils::try_deserialize_timestamp) | ||
| .unwrap_or_default(); | ||
|
|
||
| oci_dir.push_layer_full( | ||
| &mut new_manifest, | ||
| &mut new_config, | ||
| layer, | ||
| previous_annotations, | ||
| previous_description, | ||
| previous_created, | ||
| ); | ||
| } | ||
|
|
||
| let descriptor = oci_dir.write_config(new_config).context("Writing config")?; | ||
|
|
||
| new_manifest.set_config(descriptor); | ||
| oci_dir | ||
| .insert_manifest(new_manifest, None, Platform::default()) | ||
| .context("Writing manifest")?; | ||
|
|
||
| // Pass the temporary oci directory as the current working directory for the skopeo process | ||
| let tempoci = ostree_ext::container::ImageReference { | ||
| transport: Transport::OciDir, | ||
| name: format!("/proc/self/fd/{}", tmpdir.as_raw_fd()), | ||
| }; | ||
|
|
||
| skopeo::copy( | ||
| &tempoci, | ||
| &dest_imgref, | ||
| None, | ||
| Some(( | ||
| std::sync::Arc::new(tmpdir.try_clone()?.into()), | ||
| tmpdir.as_raw_fd(), | ||
| )), | ||
| true, | ||
| ) | ||
| .await?; | ||
|
|
||
| Ok(()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to this change but it'd be cleaner to parse this back into the
Transportenum which already handles this canonicalization