Skip to content

Commit c2dc021

Browse files
authored
Merge pull request #1421 from jlebon/pr/reproducible-pull
Make `ostree container image pull` merge commit reproducible
2 parents 3a0c6ab + d47956a commit c2dc021

File tree

6 files changed

+85
-73
lines changed

6 files changed

+85
-73
lines changed

lib/src/deploy.rs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,7 @@ pub(crate) async fn prepare_for_pull(
376376

377377
#[context("Pulling")]
378378
pub(crate) async fn pull_from_prepared(
379-
repo: &ostree::Repo,
380379
imgref: &ImageReference,
381-
target_imgref: Option<&OstreeImageReference>,
382380
quiet: bool,
383381
prog: ProgressWriter,
384382
mut prepared_image: PreparedImportMeta,
@@ -424,11 +422,9 @@ pub(crate) async fn pull_from_prepared(
424422
let import = import?;
425423
let imgref_canonicalized = imgref.clone().canonicalize()?;
426424
tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
427-
let ostree_imgref = &OstreeImageReference::from(imgref_canonicalized);
428-
let wrote_imgref = target_imgref.as_ref().unwrap_or(&ostree_imgref);
429425

430426
if let Some(msg) =
431-
ostree_container::store::image_filtered_content_warning(repo, &wrote_imgref.imgref)
427+
ostree_container::store::image_filtered_content_warning(&import.filtered_files)
432428
.context("Image content warning")?
433429
{
434430
crate::journal::journal_print(libsystemd::logging::Priority::Notice, &msg);
@@ -446,15 +442,9 @@ pub(crate) async fn pull(
446442
) -> Result<Box<ImageState>> {
447443
match prepare_for_pull(repo, imgref, target_imgref).await? {
448444
PreparedPullResult::AlreadyPresent(existing) => Ok(existing),
449-
PreparedPullResult::Ready(prepared_image_meta) => Ok(pull_from_prepared(
450-
repo,
451-
imgref,
452-
target_imgref,
453-
quiet,
454-
prog,
455-
prepared_image_meta,
456-
)
457-
.await?),
445+
PreparedPullResult::Ready(prepared_image_meta) => {
446+
Ok(pull_from_prepared(imgref, quiet, prog, prepared_image_meta).await?)
447+
}
458448
}
459449
}
460450

lib/src/install.rs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -793,22 +793,15 @@ async fn install_container(
793793
let repo = &sysroot.repo();
794794
repo.set_disable_fsync(true);
795795

796-
let pulled_image =
797-
match prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref)).await? {
798-
PreparedPullResult::AlreadyPresent(existing) => existing,
799-
PreparedPullResult::Ready(image_meta) => {
800-
check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
801-
pull_from_prepared(
802-
repo,
803-
&spec_imgref,
804-
Some(&state.target_imgref),
805-
false,
806-
ProgressWriter::default(),
807-
image_meta,
808-
)
809-
.await?
810-
}
811-
};
796+
let pulled_image = match prepare_for_pull(repo, &spec_imgref, Some(&state.target_imgref))
797+
.await?
798+
{
799+
PreparedPullResult::AlreadyPresent(existing) => existing,
800+
PreparedPullResult::Ready(image_meta) => {
801+
check_disk_space(root_setup.physical_root.as_fd(), &image_meta, &spec_imgref)?;
802+
pull_from_prepared(&spec_imgref, false, ProgressWriter::default(), image_meta).await?
803+
}
804+
};
812805

813806
repo.set_disable_fsync(false);
814807

ostree-ext/src/cli.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ pub(crate) enum ContainerImageOpts {
238238
#[clap(value_parser = parse_imgref)]
239239
imgref: OstreeImageReference,
240240

241+
/// File to which to write the resulting OSTree commit digest
242+
#[clap(long)]
243+
ostree_digestfile: Option<Utf8PathBuf>,
244+
241245
#[clap(flatten)]
242246
proxyopts: ContainerProxyOpts,
243247

@@ -863,13 +867,15 @@ async fn container_info(imgref: &OstreeImageReference) -> Result<()> {
863867
async fn container_store(
864868
repo: &ostree::Repo,
865869
imgref: &OstreeImageReference,
870+
ostree_digestfile: Option<Utf8PathBuf>,
866871
proxyopts: ContainerProxyOpts,
867872
quiet: bool,
868873
check: Option<Utf8PathBuf>,
869874
) -> Result<()> {
870875
let mut imp = ImageImporter::new(repo, imgref, proxyopts.into()).await?;
871876
let prep = match imp.prepare().await? {
872877
PrepareResult::AlreadyPresent(c) => {
878+
write_digest_file(ostree_digestfile, &c.merge_commit)?;
873879
println!("No changes in {} => {}", imgref, c.merge_commit);
874880
return Ok(());
875881
}
@@ -906,17 +912,26 @@ async fn container_store(
906912
}
907913
let import = import?;
908914
if let Some(msg) =
909-
ostree_container::store::image_filtered_content_warning(repo, &imgref.imgref)?
915+
ostree_container::store::image_filtered_content_warning(&import.filtered_files)?
910916
{
911917
eprintln!("{msg}")
912918
}
913919
if let Some(ref text) = import.verify_text {
914920
println!("{text}");
915921
}
922+
write_digest_file(ostree_digestfile, &import.merge_commit)?;
916923
println!("Wrote: {} => {}", imgref, import.merge_commit);
917924
Ok(())
918925
}
919926

927+
fn write_digest_file(digestfile: Option<Utf8PathBuf>, digest: &str) -> Result<()> {
928+
if let Some(digestfile) = digestfile.as_deref() {
929+
let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
930+
rootfs.write(digestfile.as_str().trim_start_matches('/'), digest)?;
931+
}
932+
Ok(())
933+
}
934+
920935
/// Output the container image history
921936
async fn container_history(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
922937
let img = crate::container::store::query_image(repo, imgref)?
@@ -1095,12 +1110,14 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
10951110
ContainerImageOpts::Pull {
10961111
repo,
10971112
imgref,
1113+
ostree_digestfile,
10981114
proxyopts,
10991115
quiet,
11001116
check,
11011117
} => {
11021118
let repo = parse_repo(&repo)?;
1103-
container_store(&repo, &imgref, proxyopts, quiet, check).await
1119+
container_store(&repo, &imgref, ostree_digestfile, proxyopts, quiet, check)
1120+
.await
11041121
}
11051122
ContainerImageOpts::Reexport {
11061123
repo,
@@ -1263,7 +1280,6 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
12631280
ostree::Sysroot::new_default()
12641281
};
12651282
sysroot.load(gio::Cancellable::NONE)?;
1266-
let repo = &sysroot.repo();
12671283
let kargs = karg.as_deref();
12681284
let kargs = kargs.map(|v| {
12691285
let r: Vec<_> = v.iter().map(|s| s.as_str()).collect();
@@ -1321,10 +1337,8 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
13211337
Some(options),
13221338
)
13231339
.await?;
1324-
let wrote_imgref = target_imgref.as_ref().unwrap_or(&imgref);
13251340
if let Some(msg) = ostree_container::store::image_filtered_content_warning(
1326-
repo,
1327-
&wrote_imgref.imgref,
1341+
&state.filtered_files,
13281342
)? {
13291343
eprintln!("{msg}")
13301344
}

ostree-ext/src/container/store.rs

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@ pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest";
5858
const META_MANIFEST: &str = "ostree.manifest";
5959
/// The key injected into the merge commit with the image configuration serialized as JSON.
6060
const META_CONFIG: &str = "ostree.container.image-config";
61-
/// Value of type `a{sa{su}}` containing number of filtered out files
62-
pub const META_FILTERED: &str = "ostree.tar-filtered";
63-
/// The type used to store content filtering information with `META_FILTERED`.
61+
/// The type used to store content filtering information.
6462
pub type MetaFilteredData = HashMap<String, HashMap<String, u32>>;
6563

6664
/// The ref prefixes which point to ostree deployments. (TODO: Add an official API for this)
@@ -138,6 +136,8 @@ pub struct LayeredImageState {
138136
/// in the future we should probably instead just proxy a signature object
139137
/// instead, but this is sufficient for now.
140138
pub verify_text: Option<String>,
139+
/// Files that were filtered out during the import.
140+
pub filtered_files: Option<MetaFilteredData>,
141141
}
142142

143143
impl LayeredImageState {
@@ -969,7 +969,7 @@ impl ImageImporter {
969969
let ostree_ref = ref_for_image(&target_imgref.imgref)?;
970970

971971
let mut layer_commits = Vec::new();
972-
let mut layer_filtered_content: MetaFilteredData = HashMap::new();
972+
let mut layer_filtered_content: Option<MetaFilteredData> = None;
973973
let have_derived_layers = !import.layers.is_empty();
974974
tracing::debug!("Processing layers: {}", import.layers.len());
975975
for layer in import.layers {
@@ -1024,7 +1024,9 @@ impl ImageImporter {
10241024
write!(msg, "...and {n_rest} more").unwrap();
10251025
}
10261026
tracing::debug!("Found filtered toplevels: {msg}");
1027-
layer_filtered_content.insert(layer.layer.digest().to_string(), filtered_owned);
1027+
layer_filtered_content
1028+
.get_or_insert_default()
1029+
.insert(layer.layer.digest().to_string(), filtered_owned);
10281030
} else {
10291031
tracing::debug!("No filtered content");
10301032
}
@@ -1047,7 +1049,7 @@ impl ImageImporter {
10471049
let _ = self.layer_byte_progress.take();
10481050
let _ = self.layer_progress.take();
10491051

1050-
let mut metadata = HashMap::new();
1052+
let mut metadata = BTreeMap::new();
10511053
metadata.insert(
10521054
META_MANIFEST_DIGEST,
10531055
import.manifest_digest.to_string().to_variant(),
@@ -1064,8 +1066,6 @@ impl ImageImporter {
10641066
"ostree.importer.version",
10651067
env!("CARGO_PKG_VERSION").to_variant(),
10661068
);
1067-
let filtered = layer_filtered_content.to_variant();
1068-
metadata.insert(META_FILTERED, filtered);
10691069
let metadata = metadata.to_variant();
10701070

10711071
let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
@@ -1191,6 +1191,7 @@ impl ImageImporter {
11911191
.await?;
11921192
// We can at least avoid re-verifying the base commit.
11931193
state.verify_text = import.verify_text;
1194+
state.filtered_files = layer_filtered_content;
11941195
Ok(state)
11951196
}
11961197
}
@@ -1354,6 +1355,7 @@ pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<Layer
13541355
cached_update,
13551356
// we can't cross-reference with a remote here
13561357
verify_text: None,
1358+
filtered_files: None,
13571359
});
13581360
tracing::debug!("Wrote merge commit {}", state.merge_commit);
13591361
Ok(state)
@@ -1726,36 +1728,26 @@ pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
17261728
Ok(n as u32)
17271729
}
17281730

1729-
/// Given an image, if it has any non-ostree compatible content, return a suitable
1730-
/// warning message.
1731+
/// Generate a suitable warning message from given list of filtered files, if any.
17311732
pub fn image_filtered_content_warning(
1732-
repo: &ostree::Repo,
1733-
image: &ImageReference,
1733+
filtered_files: &Option<MetaFilteredData>,
17341734
) -> Result<Option<String>> {
17351735
use std::fmt::Write;
17361736

1737-
let ostree_ref = ref_for_image(image)?;
1738-
let rev = repo.require_rev(&ostree_ref)?;
1739-
let commit_obj = repo.load_commit(rev.as_str())?.0;
1740-
let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1741-
1742-
let r = commit_meta
1743-
.lookup::<MetaFilteredData>(META_FILTERED)?
1744-
.filter(|v| !v.is_empty())
1745-
.map(|v| {
1746-
let mut filtered = HashMap::<&String, u32>::new();
1747-
for paths in v.values() {
1748-
for (k, v) in paths {
1749-
let e = filtered.entry(k).or_default();
1750-
*e += v;
1751-
}
1737+
let r = filtered_files.as_ref().map(|v| {
1738+
let mut filtered = BTreeMap::<&String, u32>::new();
1739+
for paths in v.values() {
1740+
for (k, v) in paths {
1741+
let e = filtered.entry(k).or_default();
1742+
*e += v;
17521743
}
1753-
let mut buf = "Image contains non-ostree compatible file paths:".to_string();
1754-
for (k, v) in filtered {
1755-
write!(buf, " {k}: {v}").unwrap();
1756-
}
1757-
buf
1758-
});
1744+
}
1745+
let mut buf = "Image contains non-ostree compatible file paths:".to_string();
1746+
for (k, v) in filtered {
1747+
write!(buf, " {k}: {v}").unwrap();
1748+
}
1749+
buf
1750+
});
17591751
Ok(r)
17601752
}
17611753

ostree-ext/src/fixture.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,8 @@ impl Fixture {
939939
temprootd.create_dir_with("usr/bin", &db)?;
940940
temprootd.write("usr/bin/newderivedfile", "newderivedfile v0")?;
941941
temprootd.write("usr/bin/newderivedfile3", "newderivedfile3 v0")?;
942+
temprootd.create_dir_with("run", &db)?;
943+
temprootd.write("run/filtered", "data")?;
942944
crate::integrationtest::generate_derived_oci(derived_path, temproot, tag)?;
943945
Ok(())
944946
}

ostree-ext/tests/it/main.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@ async fn test_container_chunked() -> Result<()> {
10311031
assert_eq!(store::list_images(fixture.destrepo()).unwrap().len(), 1);
10321032

10331033
assert!(
1034-
store::image_filtered_content_warning(fixture.destrepo(), &imgref.imgref)
1034+
store::image_filtered_content_warning(&import.filtered_files)
10351035
.unwrap()
10361036
.is_none()
10371037
);
@@ -1122,11 +1122,32 @@ r usr/bin/bash bash-v0
11221122

11231123
// We want to test explicit layer pruning
11241124
imp.disable_gc();
1125-
let _import = imp.import(prep).await.unwrap();
1125+
let import = imp.import(prep).await.unwrap();
11261126
assert_eq!(store::list_images(fixture.destrepo()).unwrap().len(), 2);
11271127

1128+
assert_eq!(
1129+
store::image_filtered_content_warning(&import.filtered_files)
1130+
.unwrap()
1131+
.unwrap(),
1132+
"Image contains non-ostree compatible file paths: filtered: 1"
1133+
);
1134+
1135+
// redo it but with the layers already imported and sanity-check that the
1136+
// merge commit is the same
1137+
let merge_commit = import.merge_commit;
1138+
store::remove_image(fixture.destrepo(), &derived_imgref.imgref).unwrap();
1139+
let mut imp =
1140+
store::ImageImporter::new(fixture.destrepo(), &derived_imgref, Default::default()).await?;
1141+
let prep = match imp.prepare().await.unwrap() {
1142+
store::PrepareResult::AlreadyPresent(_) => panic!("should not be already imported"),
1143+
store::PrepareResult::Ready(r) => r,
1144+
};
1145+
let import = imp.import(prep).await.context("reimport").unwrap();
1146+
assert_eq!(import.merge_commit, merge_commit);
1147+
1148+
// but this time we don't get the warning because we didn't reimport the derived layer
11281149
assert!(
1129-
store::image_filtered_content_warning(fixture.destrepo(), &derived_imgref.imgref)
1150+
store::image_filtered_content_warning(&import.filtered_files)
11301151
.unwrap()
11311152
.is_none()
11321153
);
@@ -1225,7 +1246,7 @@ async fn test_container_var_content() -> Result<()> {
12251246
.query_exists(gio::Cancellable::NONE));
12261247

12271248
assert!(
1228-
store::image_filtered_content_warning(fixture.destrepo(), &derived_imgref.imgref)
1249+
store::image_filtered_content_warning(&import.filtered_files)
12291250
.unwrap()
12301251
.is_none()
12311252
);

0 commit comments

Comments
 (0)