Skip to content

Commit 1d344dc

Browse files
composefs/export: Use ocidir for creating OCI compliant directory
Instead of handling the history,metadata,annotations ourselves, delegate them to `ocidir` crate. Also take into account the source and target image references Finally call `skopeo::copy` to copy to containers-storage Signed-off-by: Pragyan Poudyal <[email protected]>
1 parent 88e332f commit 1d344dc

File tree

9 files changed

+157
-159
lines changed

9 files changed

+157
-159
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ liboverdrop = "0.1.0"
6262
libsystemd = "0.7"
6363
linkme = "0.3"
6464
nom = "8.0.0"
65+
ocidir = "0.6.0"
6566
schemars = { version = "1.0.4", features = ["chrono04"] }
6667
serde_ignored = "0.1.10"
6768
serde_yaml = "0.9.34"
Lines changed: 139 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
1-
#![allow(dead_code, unused_variables)]
2-
3-
use std::io::{Read, Seek, Write};
1+
use std::{fs::File, io::Read, os::fd::AsRawFd};
42

53
use anyhow::{Context, Result};
6-
use canon_json::CanonJsonSerialize;
7-
use cap_std_ext::cap_std::{
8-
ambient_authority,
9-
fs::{Dir, MetadataExt, OpenOptions},
10-
};
4+
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
115
use composefs::{
126
fsverity::FsVerityHashValue,
137
splitstream::{SplitStreamData, SplitStreamReader},
148
tree::{LeafContent, RegularFile},
159
};
1610
use composefs_oci::tar::TarItem;
17-
use openssl::sha::Sha256;
18-
use ostree_ext::oci_spec::image::{Descriptor, Digest, ImageConfiguration, MediaType};
11+
use ocidir::{oci_spec::image::Platform, OciDir};
12+
use ostree_ext::container::skopeo;
13+
use ostree_ext::{container::Transport, oci_spec::image::ImageConfiguration};
1914
use tar::{EntryType, Header};
2015

2116
use crate::{
2217
bootc_composefs::{
2318
status::{get_composefs_status, get_imginfo},
2419
update::str_to_sha256digest,
2520
},
21+
image::IMAGE_DEFAULT,
2622
store::{BootedComposefs, Storage},
2723
};
2824

@@ -58,10 +54,16 @@ fn get_entry_with_header<R: Read, ObjectID: FsVerityHashValue>(
5854
}
5955
}
6056

61-
pub async fn export_repo_to_oci(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<()> {
57+
/// Exports a composefs repository to a container image in containers-storage:
58+
pub async fn export_repo_to_image(
59+
storage: &Storage,
60+
booted_cfs: &BootedComposefs,
61+
source: Option<&str>,
62+
target: Option<&str>,
63+
) -> Result<()> {
6264
let host = get_composefs_status(storage, booted_cfs).await?;
6365

64-
let image = host
66+
let booted_image = host
6567
.status
6668
.booted
6769
.as_ref()
@@ -70,27 +72,61 @@ pub async fn export_repo_to_oci(storage: &Storage, booted_cfs: &BootedComposefs)
7072
.as_ref()
7173
.unwrap();
7274

73-
let imginfo = get_imginfo(
74-
storage,
75-
&booted_cfs.cmdline.digest,
76-
// TODO: Make this optional
77-
&image.image,
78-
)
79-
.await?;
75+
// If the target isn't specified, push to containers-storage + our default image
76+
let dest_imgref = match target {
77+
Some(target) => ostree_ext::container::ImageReference {
78+
transport: Transport::ContainerStorage,
79+
name: target.to_owned(),
80+
},
81+
None => ostree_ext::container::ImageReference {
82+
transport: Transport::ContainerStorage,
83+
name: IMAGE_DEFAULT.into(),
84+
},
85+
};
86+
87+
// If the source isn't specified, we use the booted image
88+
let source = match source {
89+
Some(source) => ostree_ext::container::ImageReference::try_from(source)
90+
.context("Parsing source image")?,
91+
92+
None => ostree_ext::container::ImageReference {
93+
transport: Transport::try_from(booted_image.image.transport.as_str()).unwrap(),
94+
name: booted_image.image.image.clone(),
95+
},
96+
};
97+
98+
let mut depl_verity = None;
99+
100+
for depl in host
101+
.status
102+
.booted
103+
.iter()
104+
.chain(host.status.staged.iter())
105+
.chain(host.status.rollback.iter())
106+
.chain(host.status.other_deployments.iter())
107+
{
108+
let img = &depl.image.as_ref().unwrap().image;
109+
110+
// Not checking transport here as we'll be pulling from the repo anyway
111+
// So, image name is all we need
112+
if img.image == source.name {
113+
depl_verity = Some(depl.require_composefs()?.verity.clone());
114+
break;
115+
}
116+
}
117+
118+
let depl_verity = depl_verity.ok_or_else(|| anyhow::anyhow!("Image {source} not found"))?;
80119

81-
let config_name = &image.image_digest;
82-
let config_name = str_to_sha256digest(&config_name)?;
120+
let imginfo = get_imginfo(storage, &depl_verity, None).await?;
121+
122+
let config_name = &imginfo.manifest.config().digest().digest();
123+
let config_name = str_to_sha256digest(config_name)?;
83124

84125
let var_tmp =
85126
Dir::open_ambient_dir("/var/tmp", ambient_authority()).context("Opening /var/tmp")?;
86127

87-
var_tmp
88-
.create_dir_all(&*booted_cfs.cmdline.digest)
89-
.context("Creating image dir")?;
90-
91-
let image_dir = var_tmp
92-
.open_dir(&*booted_cfs.cmdline.digest)
93-
.context("Opening image dir")?;
128+
let tmpdir = cap_std_ext::cap_tempfile::tempdir_in(&var_tmp)?;
129+
let oci_dir = OciDir::ensure(tmpdir.try_clone()?).context("Opening OCI")?;
94130

95131
let mut config_stream = booted_cfs
96132
.repo
@@ -99,7 +135,8 @@ pub async fn export_repo_to_oci(storage: &Storage, booted_cfs: &BootedComposefs)
99135

100136
let config = ImageConfiguration::from_reader(&mut config_stream)?;
101137

102-
// We can't guarantee that we'll get the same tar as the container image
138+
// We can't guarantee that we'll get the same tar stream as the container image
139+
// So we create new config and manifest
103140
let mut new_config = config.clone();
104141
if let Some(history) = new_config.history_mut() {
105142
history.clear();
@@ -109,163 +146,113 @@ pub async fn export_repo_to_oci(storage: &Storage, booted_cfs: &BootedComposefs)
109146
let mut new_manifest = imginfo.manifest.clone();
110147
new_manifest.layers_mut().clear();
111148

112-
let mut file_open_opts = OpenOptions::new();
113-
file_open_opts.write(true).create(true);
114-
115-
for (idx, diff_id) in config.rootfs().diff_ids().iter().enumerate() {
116-
let layer_sha256 = str_to_sha256digest(diff_id)?;
149+
for (idx, old_diff_id) in config.rootfs().diff_ids().iter().enumerate() {
150+
let layer_sha256 = str_to_sha256digest(old_diff_id)?;
117151
let layer_verity = config_stream.lookup(&layer_sha256)?;
118152

119153
let mut layer_stream = booted_cfs
120154
.repo
121155
.open_stream(&hex::encode(layer_sha256), Some(layer_verity))?;
122156

123-
let mut file = image_dir.open_with(hex::encode(layer_sha256), &file_open_opts)?;
124-
125-
let mut builder = tar::Builder::new(&mut file);
157+
let mut layer_writer = oci_dir.create_layer(None)?;
158+
layer_writer.follow_symlinks(false);
126159

127160
while let Some((header, entry)) = get_entry_with_header(&mut layer_stream)? {
128161
let hsize = header.size()? as usize;
129162
let mut v = vec![0; hsize];
130163

131164
match &entry {
132-
TarItem::Directory => {
133-
assert_eq!(hsize, 0);
134-
}
135-
136165
TarItem::Leaf(leaf_content) => {
137166
match &leaf_content {
138167
LeafContent::Regular(reg) => match reg {
139168
RegularFile::Inline(items) => {
140-
assert_eq!(hsize, items.len());
141169
v[..hsize].copy_from_slice(items);
142170
}
143171

144-
RegularFile::External(obj_id, size) => {
145-
assert_eq!(*size as usize, hsize);
146-
147-
let mut file =
148-
std::fs::File::from(booted_cfs.repo.open_object(obj_id)?);
149-
172+
RegularFile::External(obj_id, ..) => {
173+
let mut file = File::from(booted_cfs.repo.open_object(obj_id)?);
150174
file.read_exact(&mut v)?;
151175
}
152176
},
153177

154-
LeafContent::BlockDevice(_) => todo!(),
155-
LeafContent::CharacterDevice(_) => {
156-
todo!()
157-
}
158-
LeafContent::Fifo => todo!(),
159-
LeafContent::Socket => todo!(),
160-
161-
LeafContent::Symlink(..) => {
162-
// we don't need to write the data for symlinks as the
163-
// target will be in the header itself
164-
assert_eq!(hsize, 0);
165-
}
178+
// we don't need to write the data for symlinks.
179+
// Same goes for devices, fifos and sockets
180+
_ => {}
166181
}
167182
}
168183

169-
TarItem::Hardlink(..) => {
170-
// we don't need to write the data for hardlinks as the
171-
// target will be in the header itself
172-
assert_eq!(hsize, 0);
173-
}
184+
// we don't need to write the data for hardlinks/dirs
185+
TarItem::Directory | TarItem::Hardlink(..) => {}
174186
};
175187

176-
builder
188+
layer_writer
177189
.append(&header, v.as_slice())
178190
.context("Failed to write entry")?;
179191
}
180192

181-
builder.finish().context("Finishing builder")?;
182-
drop(builder);
183-
184-
let mut new_diff_id = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
185-
186-
file.seek(std::io::SeekFrom::Start(0))
187-
.context("Seek failed")?;
188-
std::io::copy(&mut file, &mut new_diff_id).context("Failed to compute hash")?;
189-
190-
let final_sha = new_diff_id.finish()?;
191-
let final_sha_str = hex::encode(final_sha);
192-
193-
rustix::fs::renameat(&image_dir, hex::encode(layer_sha256), &image_dir, &final_sha_str)
194-
.context("Renameat")?;
195-
196-
let digest = format!("sha256:{}", hex::encode(final_sha));
197-
198-
new_config.rootfs_mut().diff_ids_mut().push(digest.clone());
199-
200-
// TODO: Gzip this for manifest
201-
new_manifest.layers_mut().push(Descriptor::new(
202-
MediaType::ImageLayer,
203-
file.metadata()?.size(),
204-
Digest::try_from(digest)?,
205-
));
206-
207-
if let Some(old_history) = &config.history() {
208-
if idx >= old_history.len() {
209-
anyhow::bail!("Found more layers than history");
210-
}
211-
212-
let old_history = &old_history[idx];
213-
214-
let mut history = ostree_ext::oci_spec::image::HistoryBuilder::default();
215-
216-
if let Some(old_created) = old_history.created() {
217-
history = history.created(old_created);
218-
}
219-
220-
if let Some(old_created_by) = old_history.created_by() {
221-
history = history.created_by(old_created_by);
222-
}
223-
224-
if let Some(comment) = old_history.comment() {
225-
history = history.comment(comment);
226-
}
227-
228-
new_config
229-
.history_mut()
230-
.get_or_insert(Vec::new())
231-
.push(history.build().unwrap());
232-
}
233-
234-
// TODO: Fsync
193+
layer_writer.finish()?;
194+
195+
let layer = layer_writer
196+
.into_inner()
197+
.context("Getting inner layer writer")?
198+
.complete()
199+
.context("Writing layer to disk")?;
200+
201+
tracing::debug!("Wrote layer: {}", layer.uncompressed_sha256_as_digest());
202+
203+
let previous_annotations = imginfo
204+
.manifest
205+
.layers()
206+
.get(idx)
207+
.and_then(|l| l.annotations().as_ref())
208+
.cloned();
209+
210+
let history = imginfo.config.history().as_ref();
211+
let history_entry = history.and_then(|v| v.get(idx));
212+
let previous_description = history_entry
213+
.clone()
214+
.and_then(|h| h.comment().as_deref())
215+
.unwrap_or_default();
216+
217+
let previous_created = history_entry
218+
.and_then(|h| h.created().as_deref())
219+
.and_then(bootc_utils::try_deserialize_timestamp)
220+
.unwrap_or_default();
221+
222+
oci_dir.push_layer_full(
223+
&mut new_manifest,
224+
&mut new_config,
225+
layer,
226+
previous_annotations,
227+
previous_description,
228+
previous_created,
229+
);
235230
}
236231

237-
let config_json = new_config.to_canon_json_vec()?;
238-
239-
// Hash the new config
240-
let mut config_hash = Sha256::new();
241-
config_hash.update(&config_json);
242-
let config_hash = hex::encode(config_hash.finish());
243-
244-
// Write the config to Directory
245-
let mut cfg_file = image_dir
246-
.open_with(&config_hash, &file_open_opts)
247-
.context("Opening config file")?;
248-
249-
cfg_file
250-
.write_all(&config_json)
251-
.context("Failed to write config")?;
252-
253-
// Write the manifest
254-
let mut manifest_file = image_dir
255-
.open_with("manifest.json", &file_open_opts)
256-
.context("Opening manifest file")?;
257-
258-
new_manifest.set_config(Descriptor::new(
259-
MediaType::ImageConfig,
260-
config_json.len() as u64,
261-
Digest::try_from(format!("sha256:{config_hash}"))?,
262-
));
263-
264-
manifest_file
265-
.write_all(&new_manifest.to_canon_json_vec()?)
266-
.context("Failed to write manifest")?;
267-
268-
println!("Image: {config_hash}");
232+
let descriptor = oci_dir.write_config(new_config).context("Writing config")?;
233+
234+
new_manifest.set_config(descriptor);
235+
oci_dir
236+
.insert_manifest(new_manifest, None, Platform::default())
237+
.context("Writing manifest")?;
238+
239+
// Pass the temporary oci directory as the current working directory for the skopeo process
240+
let tempoci = ostree_ext::container::ImageReference {
241+
transport: Transport::OciDir,
242+
name: format!("/proc/self/fd/{}", tmpdir.as_raw_fd()),
243+
};
244+
245+
skopeo::copy(
246+
&tempoci,
247+
&dest_imgref,
248+
None,
249+
Some((
250+
std::sync::Arc::new(tmpdir.try_clone()?.into()),
251+
tmpdir.as_raw_fd(),
252+
)),
253+
true,
254+
)
255+
.await?;
269256

270257
Ok(())
271258
}

0 commit comments

Comments
 (0)