Skip to content

Commit 04aaa2e

Browse files
copy-to-storage: Refactor tar parsing
Inline the tar parsing/unpacking Check for two NULL 512 blocks instead of just one Share source image and target image generating code between composefs and ostree Signed-off-by: Pragyan Poudyal <[email protected]>
1 parent 72d7942 commit 04aaa2e

File tree

4 files changed

+120
-127
lines changed

4 files changed

+120
-127
lines changed

crates/lib/src/bootc_composefs/export.rs

Lines changed: 63 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,22 @@
1-
use std::{fs::File, io::Read, os::fd::AsRawFd};
1+
use std::{fs::File, os::fd::AsRawFd};
22

33
use anyhow::{Context, Result};
44
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
5-
use composefs::{
6-
fsverity::FsVerityHashValue,
7-
splitstream::{SplitStreamData, SplitStreamReader},
8-
tree::{LeafContent, RegularFile},
9-
};
10-
use composefs_oci::tar::TarItem;
5+
use composefs::splitstream::SplitStreamData;
116
use ocidir::{oci_spec::image::Platform, OciDir};
127
use ostree_ext::container::skopeo;
138
use ostree_ext::{container::Transport, oci_spec::image::ImageConfiguration};
14-
use tar::{EntryType, Header};
9+
use tar::EntryType;
1510

11+
use crate::image::get_imgrefs_for_copy;
1612
use crate::{
1713
bootc_composefs::{
1814
status::{get_composefs_status, get_imginfo},
1915
update::str_to_sha256digest,
2016
},
21-
image::IMAGE_DEFAULT,
2217
store::{BootedComposefs, Storage},
2318
};
2419

25-
fn get_entry_with_header<R: Read, ObjectID: FsVerityHashValue>(
26-
reader: &mut SplitStreamReader<R, ObjectID>,
27-
) -> anyhow::Result<Option<(Header, TarItem<ObjectID>)>> {
28-
let mut buf = [0u8; 512];
29-
if !reader.read_inline_exact(&mut buf)? || buf == [0u8; 512] {
30-
return Ok(None);
31-
}
32-
33-
let header = tar::Header::from_byte_slice(&buf);
34-
35-
let size = header.entry_size()?;
36-
37-
let item = match reader.read_exact(size as usize, ((size + 511) & !511) as usize)? {
38-
SplitStreamData::External(id) => match header.entry_type() {
39-
EntryType::Regular | EntryType::Continuous => {
40-
TarItem::Leaf(LeafContent::Regular(RegularFile::External(id, size)))
41-
}
42-
_ => anyhow::bail!("Unsupported external-chunked entry {header:?} {id:?}"),
43-
},
44-
45-
SplitStreamData::Inline(content) => match header.entry_type() {
46-
EntryType::Directory => TarItem::Directory,
47-
// We do not care what the content is as we're re-archiving it anyway
48-
_ => TarItem::Leaf(LeafContent::Regular(RegularFile::Inline(content))),
49-
},
50-
};
51-
52-
return Ok(Some((header.clone(), item)));
53-
}
54-
5520
/// Exports a composefs repository to a container image in containers-storage:
5621
pub async fn export_repo_to_image(
5722
storage: &Storage,
@@ -61,37 +26,7 @@ pub async fn export_repo_to_image(
6126
) -> Result<()> {
6227
let host = get_composefs_status(storage, booted_cfs).await?;
6328

64-
let booted_image = host
65-
.status
66-
.booted
67-
.as_ref()
68-
.ok_or_else(|| anyhow::anyhow!("Booted deployment not found"))?
69-
.image
70-
.as_ref()
71-
.unwrap();
72-
73-
// If the target isn't specified, push to containers-storage + our default image
74-
let dest_imgref = match target {
75-
Some(target) => ostree_ext::container::ImageReference {
76-
transport: Transport::ContainerStorage,
77-
name: target.to_owned(),
78-
},
79-
None => ostree_ext::container::ImageReference {
80-
transport: Transport::ContainerStorage,
81-
name: IMAGE_DEFAULT.into(),
82-
},
83-
};
84-
85-
// If the source isn't specified, we use the booted image
86-
let source = match source {
87-
Some(source) => ostree_ext::container::ImageReference::try_from(source)
88-
.context("Parsing source image")?,
89-
90-
None => ostree_ext::container::ImageReference {
91-
transport: Transport::try_from(booted_image.image.transport.as_str()).unwrap(),
92-
name: booted_image.image.image.clone(),
93-
},
94-
};
29+
let (source, dest_imgref) = get_imgrefs_for_copy(source, target).await?;
9530

9631
let mut depl_verity = None;
9732

@@ -144,6 +79,8 @@ pub async fn export_repo_to_image(
14479
let mut new_manifest = imginfo.manifest.clone();
14580
new_manifest.layers_mut().clear();
14681

82+
let total_layers = config.rootfs().diff_ids().len();
83+
14784
for (idx, old_diff_id) in config.rootfs().diff_ids().iter().enumerate() {
14885
let layer_sha256 = str_to_sha256digest(old_diff_id)?;
14986
let layer_verity = config_stream.lookup(&layer_sha256)?;
@@ -155,37 +92,60 @@ pub async fn export_repo_to_image(
15592
let mut layer_writer = oci_dir.create_layer(None)?;
15693
layer_writer.follow_symlinks(false);
15794

158-
while let Some((header, entry)) = get_entry_with_header(&mut layer_stream)? {
159-
let hsize = header.size()? as usize;
160-
let mut v = vec![0; hsize];
161-
162-
match &entry {
163-
TarItem::Leaf(leaf_content) => {
164-
match &leaf_content {
165-
LeafContent::Regular(reg) => match reg {
166-
RegularFile::Inline(items) => {
167-
v[..hsize].copy_from_slice(items);
168-
}
169-
170-
RegularFile::External(obj_id, ..) => {
171-
let mut file = File::from(booted_cfs.repo.open_object(obj_id)?);
172-
file.read_exact(&mut v)?;
173-
}
174-
},
175-
176-
// we don't need to write the data for symlinks.
177-
// Same goes for devices, fifos and sockets
178-
_ => {}
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")?;
179131
}
180-
}
181132

182-
// we don't need to write the data for hardlinks/dirs
183-
TarItem::Directory | TarItem::Hardlink(..) => {}
184-
};
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+
}
185140

186-
layer_writer
187-
.append(&header, v.as_slice())
188-
.context("Failed to write entry")?;
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+
};
189149
}
190150

191151
layer_writer.finish()?;
@@ -196,7 +156,11 @@ pub async fn export_repo_to_image(
196156
.complete()
197157
.context("Writing layer to disk")?;
198158

199-
tracing::debug!("Wrote layer: {}", layer.uncompressed_sha256_as_digest());
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+
);
200164

201165
let previous_annotations = imginfo
202166
.manifest

crates/lib/src/cli.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1592,7 +1592,12 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15921592

15931593
match storage.kind()? {
15941594
BootedStorageKind::Ostree(..) => {
1595-
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
1595+
crate::image::push_entrypoint(
1596+
&storage,
1597+
source.as_deref(),
1598+
target.as_deref(),
1599+
)
1600+
.await
15961601
}
15971602
BootedStorageKind::Composefs(booted) => {
15981603
bootc_composefs::export::export_repo_to_image(

crates/lib/src/image.rs

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use crate::{
1515
boundimage::query_bound_images,
1616
cli::{ImageListFormat, ImageListType},
1717
podstorage::CStorage,
18+
status::get_host,
19+
store::Storage,
1820
utils::async_task_with_spinner,
1921
};
2022

@@ -139,43 +141,65 @@ pub(crate) async fn list_entrypoint(
139141
Ok(())
140142
}
141143

142-
/// Implementation of `bootc image push-to-storage`.
143-
#[context("Pushing image")]
144-
pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>) -> Result<()> {
144+
/// Returns the source and target ImageReference
145+
/// If the source isn't specified, we use booted image
146+
/// If the target isn't specified, we push to containers-storage with our default image
147+
pub(crate) async fn get_imgrefs_for_copy(
148+
source: Option<&str>,
149+
target: Option<&str>,
150+
) -> Result<(ImageReference, ImageReference)> {
145151
// Initialize floating c_storage early - needed for container operations
146152
crate::podstorage::ensure_floating_c_storage_initialized();
147153

148-
let transport = Transport::ContainerStorage;
149-
let sysroot = crate::cli::get_storage().await?;
150-
let ostree = sysroot.get_ostree()?;
151-
let repo = &ostree.repo();
152-
153154
// If the target isn't specified, push to containers-storage + our default image
154-
let target = if let Some(target) = target {
155-
ImageReference {
156-
transport,
155+
let dest_imgref = match target {
156+
Some(target) => ostree_ext::container::ImageReference {
157+
transport: Transport::ContainerStorage,
157158
name: target.to_owned(),
158-
}
159-
} else {
160-
ImageReference {
159+
},
160+
None => ostree_ext::container::ImageReference {
161161
transport: Transport::ContainerStorage,
162-
name: IMAGE_DEFAULT.to_string(),
163-
}
162+
name: IMAGE_DEFAULT.into(),
163+
},
164164
};
165165

166166
// If the source isn't specified, we use the booted image
167-
let source = if let Some(source) = source {
168-
ImageReference::try_from(source).context("Parsing source image")?
169-
} else {
170-
let status = crate::status::get_status_require_booted(&ostree)?;
171-
// SAFETY: We know it's booted
172-
let booted = status.2.status.booted.unwrap();
173-
let booted_image = booted.image.unwrap().image;
174-
ImageReference {
175-
transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
176-
name: booted_image.image,
167+
let src_imgref = match source {
168+
Some(source) => ostree_ext::container::ImageReference::try_from(source)
169+
.context("Parsing source image")?,
170+
171+
None => {
172+
let host = get_host().await?;
173+
174+
let booted = host
175+
.status
176+
.booted
177+
.ok_or_else(|| anyhow::anyhow!("Booted deployment not found"))?;
178+
179+
let booted_image = booted.image.unwrap().image;
180+
181+
ImageReference {
182+
transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
183+
name: booted_image.image,
184+
}
177185
}
178186
};
187+
188+
return Ok((src_imgref, dest_imgref));
189+
}
190+
191+
/// Implementation of `bootc image push-to-storage`.
192+
#[context("Pushing image")]
193+
pub(crate) async fn push_entrypoint(
194+
storage: &Storage,
195+
source: Option<&str>,
196+
target: Option<&str>,
197+
) -> Result<()> {
198+
let (source, target) = get_imgrefs_for_copy(source, target).await?;
199+
200+
let ostree = storage.get_ostree()?;
201+
let repo = &ostree.repo();
202+
179203
let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
180204
opts.progress_to_stdout = true;
181205
println!("Copying local image {source} to {target} ...");

crates/lib/src/status.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ pub(crate) fn get_status(
430430
Ok((deployments, host))
431431
}
432432

433-
async fn get_host() -> Result<Host> {
433+
pub(crate) async fn get_host() -> Result<Host> {
434434
let env = crate::store::Environment::detect()?;
435435
if env.needs_mount_namespace() {
436436
crate::cli::prepare_for_write()?;

0 commit comments

Comments
 (0)