use super::*;
use crate::logging::system_repo_journal_print;
use crate::refescape;
use crate::sysroot::SysrootLock;
use crate::utils::ResultExt;
use anyhow::{anyhow, Context};
use camino::{Utf8Path, Utf8PathBuf};
use cap_std_ext::cap_std::fs::Dir;
use containers_image_proxy::{ImageProxy, OpenedImage};
use fn_error_context::context;
use futures_util::TryFutureExt;
use oci_spec::image::{self as oci_image, Descriptor, History, ImageConfiguration, ImageManifest};
use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant};
use ostree::{gio, glib};
use rustix::fs::MetadataExt;
use std::collections::{BTreeSet, HashMap};
use std::iter::FromIterator;
use tokio::sync::mpsc::{Receiver, Sender};
pub use containers_image_proxy::ImageProxyConfig;
const LAYER_PREFIX: &str = "ostree/container/blob";
const IMAGE_PREFIX: &str = "ostree/container/image";
pub const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage";
pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest";
const META_MANIFEST: &str = "ostree.manifest";
const META_CONFIG: &str = "ostree.container.image-config";
pub const META_FILTERED: &str = "ostree.tar-filtered";
pub type MetaFilteredData = HashMap<String, HashMap<String, u32>>;
const OSTREE_BASE_DEPLOYMENT_REFS: &[&str] = &["ostree/0", "ostree/1"];
const RPMOSTREE_BASE_REFS: &[&str] = &["rpmostree/base"];
fn ref_for_blob_digest(d: &str) -> Result<String> {
refescape::prefix_escape_for_ref(LAYER_PREFIX, d)
}
fn ref_for_layer(l: &oci_image::Descriptor) -> Result<String> {
ref_for_blob_digest(l.digest().as_str())
}
fn ref_for_image(l: &ImageReference) -> Result<String> {
refescape::prefix_escape_for_ref(IMAGE_PREFIX, &l.to_string())
}
#[derive(Debug)]
pub enum ImportProgress {
OstreeChunkStarted(Descriptor),
OstreeChunkCompleted(Descriptor),
DerivedLayerStarted(Descriptor),
DerivedLayerCompleted(Descriptor),
}
impl ImportProgress {
pub fn is_starting(&self) -> bool {
match self {
ImportProgress::OstreeChunkStarted(_) => true,
ImportProgress::OstreeChunkCompleted(_) => false,
ImportProgress::DerivedLayerStarted(_) => true,
ImportProgress::DerivedLayerCompleted(_) => false,
}
}
}
#[derive(Debug)]
pub struct LayerProgress {
pub layer_index: usize,
pub fetched: u64,
pub total: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub struct LayeredImageState {
pub base_commit: String,
pub merge_commit: String,
pub is_layered: bool,
pub manifest_digest: String,
pub manifest: ImageManifest,
pub configuration: Option<ImageConfiguration>,
}
impl LayeredImageState {
pub fn get_commit(&self) -> &str {
if self.is_layered {
self.merge_commit.as_str()
} else {
self.base_commit.as_str()
}
}
}
#[derive(Debug)]
pub struct ImageImporter {
repo: ostree::Repo,
pub(crate) proxy: ImageProxy,
imgref: OstreeImageReference,
target_imgref: Option<OstreeImageReference>,
no_imgref: bool, disable_gc: bool, require_bootable: bool,
pub(crate) proxy_img: OpenedImage,
layer_progress: Option<Sender<ImportProgress>>,
layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
}
#[derive(Debug)]
pub enum PrepareResult {
AlreadyPresent(Box<LayeredImageState>),
Ready(Box<PreparedImport>),
}
#[derive(Debug)]
pub struct ManifestLayerState {
pub(crate) layer: oci_image::Descriptor,
pub ostree_ref: String,
pub commit: Option<String>,
}
impl ManifestLayerState {
pub fn digest(&self) -> &str {
self.layer.digest().as_str()
}
pub fn size(&self) -> u64 {
self.layer.size() as u64
}
}
#[derive(Debug)]
pub struct PreparedImport {
pub manifest_digest: String,
pub manifest: oci_image::ImageManifest,
pub config: oci_image::ImageConfiguration,
pub previous_state: Option<Box<LayeredImageState>>,
pub previous_manifest_digest: Option<String>,
pub previous_imageid: Option<String>,
pub ostree_layers: Vec<ManifestLayerState>,
pub ostree_commit_layer: ManifestLayerState,
pub layers: Vec<ManifestLayerState>,
}
impl PreparedImport {
pub fn all_layers(&self) -> impl Iterator<Item = &ManifestLayerState> {
std::iter::once(&self.ostree_commit_layer)
.chain(self.ostree_layers.iter())
.chain(self.layers.iter())
}
pub fn version(&self) -> Option<&str> {
super::version_for_config(&self.config)
}
pub fn deprecated_warning(&self) -> Option<&'static str> {
None
}
pub fn layers_with_history(
&self,
) -> impl Iterator<Item = Result<(&ManifestLayerState, &History)>> {
let truncated = std::iter::once_with(|| Err(anyhow::anyhow!("Truncated history")));
let history = self.config.history().iter().map(Ok).chain(truncated);
self.all_layers()
.zip(history)
.map(|(s, h)| h.map(|h| (s, h)))
}
pub fn layers_to_fetch(&self) -> impl Iterator<Item = Result<(&ManifestLayerState, &str)>> {
self.layers_with_history().filter_map(|r| {
r.map(|(l, h)| {
l.commit.is_none().then(|| {
let comment = h.created_by().as_deref().unwrap_or("");
(l, comment)
})
})
.transpose()
})
}
pub(crate) fn format_layer_status(&self) -> Option<String> {
let (stored, to_fetch, to_fetch_size) =
self.all_layers()
.fold((0u32, 0u32, 0u64), |(stored, to_fetch, sz), v| {
if v.commit.is_some() {
(stored + 1, to_fetch, sz)
} else {
(stored, to_fetch + 1, sz + v.size())
}
});
(to_fetch > 0).then(|| {
let size = crate::glib::format_size(to_fetch_size);
format!("layers already present: {stored}; layers needed: {to_fetch} ({size})")
})
}
}
pub(crate) fn query_layer(
repo: &ostree::Repo,
layer: oci_image::Descriptor,
) -> Result<ManifestLayerState> {
let ostree_ref = ref_for_layer(&layer)?;
let commit = repo.resolve_rev(&ostree_ref, true)?.map(|s| s.to_string());
Ok(ManifestLayerState {
layer,
ostree_ref,
commit,
})
}
#[context("Reading manifest data from commit")]
fn manifest_data_from_commitmeta(
commit_meta: &glib::VariantDict,
) -> Result<(oci_image::ImageManifest, String)> {
let digest = commit_meta
.lookup(META_MANIFEST_DIGEST)?
.ok_or_else(|| anyhow!("Missing {} metadata on merge commit", META_MANIFEST_DIGEST))?;
let manifest_bytes: String = commit_meta
.lookup::<String>(META_MANIFEST)?
.ok_or_else(|| anyhow!("Failed to find {} metadata key", META_MANIFEST))?;
let r = serde_json::from_str(&manifest_bytes)?;
Ok((r, digest))
}
fn image_config_from_commitmeta(
commit_meta: &glib::VariantDict,
) -> Result<Option<ImageConfiguration>> {
commit_meta
.lookup::<String>(META_CONFIG)?
.filter(|v| v != "null") .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg))
.transpose()
}
pub fn manifest_digest_from_commit(commit: &glib::Variant) -> Result<String> {
let commit_meta = &commit.child_value(0);
let commit_meta = &glib::VariantDict::new(Some(commit_meta));
Ok(manifest_data_from_commitmeta(commit_meta)?.1)
}
fn layer_from_diffid<'a>(
layout: ExportLayout,
manifest: &'a ImageManifest,
config: &ImageConfiguration,
diffid: &str,
) -> Result<&'a Descriptor> {
let idx = config
.rootfs()
.diff_ids()
.iter()
.position(|x| x.as_str() == diffid)
.ok_or_else(|| anyhow!("Missing {} {}", layout.label(), diffid))?;
manifest.layers().get(idx).ok_or_else(|| {
anyhow!(
"diffid position {} exceeds layer count {}",
idx,
manifest.layers().len()
)
})
}
#[context("Parsing manifest layout")]
pub(crate) fn parse_manifest_layout<'a>(
manifest: &'a ImageManifest,
config: &ImageConfiguration,
) -> Result<(&'a Descriptor, Vec<&'a Descriptor>, Vec<&'a Descriptor>)> {
let config_labels = super::labels_of(config);
let first_layer = manifest
.layers()
.get(0)
.ok_or_else(|| anyhow!("No layers in manifest"))?;
let info = config_labels.and_then(|labels| {
labels
.get(ExportLayout::V1.label())
.map(|v| (ExportLayout::V1, v))
.or_else(|| {
labels
.get(ExportLayout::V0.label())
.map(|v| (ExportLayout::V0, v))
})
});
let (layout, target_diffid) = info.ok_or_else(|| {
anyhow!(
"No {} label found, not an ostree encapsulated container",
ExportLayout::V1.label()
)
})?;
let target_layer = layer_from_diffid(layout, manifest, config, target_diffid.as_str())?;
let mut chunk_layers = Vec::new();
let mut derived_layers = Vec::new();
let mut after_target = false;
let ostree_layer = match layout {
ExportLayout::V0 => target_layer,
ExportLayout::V1 => first_layer,
};
match layout {
ExportLayout::V0 => {
let label = layout.label();
anyhow::bail!("This legacy format using the {label} label is no longer supported");
}
ExportLayout::V1 => {
for layer in manifest.layers() {
if layer == target_layer {
if after_target {
anyhow::bail!("Multiple entries for {}", layer.digest());
}
after_target = true;
if layer != ostree_layer {
chunk_layers.push(layer);
}
} else if !after_target {
if layer != ostree_layer {
chunk_layers.push(layer);
}
} else {
derived_layers.push(layer);
}
}
}
}
Ok((ostree_layer, chunk_layers, derived_layers))
}
fn timestamp_of_manifest_or_config(
manifest: &ImageManifest,
config: &ImageConfiguration,
) -> Option<u64> {
let timestamp = manifest
.annotations()
.as_ref()
.and_then(|a| a.get(oci_image::ANNOTATION_CREATED))
.or_else(|| config.created().as_ref());
timestamp
.map(|t| {
chrono::DateTime::parse_from_rfc3339(t)
.context("Failed to parse manifest timestamp")
.map(|t| t.timestamp() as u64)
})
.transpose()
.log_err_default()
}
impl ImageImporter {
#[context("Creating importer")]
pub async fn new(
repo: &ostree::Repo,
imgref: &OstreeImageReference,
mut config: ImageProxyConfig,
) -> Result<Self> {
if imgref.imgref.transport == Transport::ContainerStorage {
merge_default_container_proxy_opts_with_isolation(&mut config, None)?;
} else {
merge_default_container_proxy_opts(&mut config)?;
}
let proxy = ImageProxy::new_with_config(config).await?;
system_repo_journal_print(
repo,
libsystemd::logging::Priority::Info,
&format!("Fetching {}", imgref),
);
let proxy_img = proxy.open_image(&imgref.imgref.to_string()).await?;
let repo = repo.clone();
Ok(ImageImporter {
repo,
proxy,
proxy_img,
target_imgref: None,
no_imgref: false,
disable_gc: false,
require_bootable: false,
imgref: imgref.clone(),
layer_progress: None,
layer_byte_progress: None,
})
}
pub fn set_target(&mut self, target: &OstreeImageReference) {
self.target_imgref = Some(target.clone())
}
pub fn set_no_imgref(&mut self) {
self.no_imgref = true;
}
pub fn require_bootable(&mut self) {
self.require_bootable = true;
}
pub fn disable_gc(&mut self) {
self.disable_gc = true;
}
#[context("Preparing import")]
pub async fn prepare(&mut self) -> Result<PrepareResult> {
self.prepare_internal(false).await
}
pub fn request_progress(&mut self) -> Receiver<ImportProgress> {
assert!(self.layer_progress.is_none());
let (s, r) = tokio::sync::mpsc::channel(2);
self.layer_progress = Some(s);
r
}
pub fn request_layer_progress(
&mut self,
) -> tokio::sync::watch::Receiver<Option<LayerProgress>> {
assert!(self.layer_byte_progress.is_none());
let (s, r) = tokio::sync::watch::channel(None);
self.layer_byte_progress = Some(s);
r
}
#[context("Fetching manifest")]
pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
match &self.imgref.sigverify {
SignatureSource::ContainerPolicy if skopeo::container_policy_is_default_insecure()? => {
return Err(anyhow!("containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"));
}
SignatureSource::OstreeRemote(_) if verify_layers => {
return Err(anyhow!(
"Cannot currently verify layered containers via ostree remote"
));
}
_ => {}
}
let (manifest_digest, manifest) = self.proxy.fetch_manifest(&self.proxy_img).await?;
let new_imageid = manifest.config().digest().as_str();
let (previous_state, previous_imageid) =
if let Some(previous_state) = try_query_image_ref(&self.repo, &self.imgref.imgref)? {
if previous_state.manifest_digest == manifest_digest {
return Ok(PrepareResult::AlreadyPresent(previous_state));
}
let previous_imageid = previous_state.manifest.config().digest().as_str();
if previous_imageid == new_imageid {
return Ok(PrepareResult::AlreadyPresent(previous_state));
}
let previous_imageid = previous_imageid.to_string();
(Some(previous_state), Some(previous_imageid))
} else {
(None, None)
};
let config = self.proxy.fetch_config(&self.proxy_img).await?;
let config_labels = super::labels_of(&config);
if self.require_bootable {
let bootable_key = *ostree::METADATA_KEY_BOOTABLE;
let bootable = config_labels.map_or(false, |l| l.contains_key(bootable_key));
if !bootable {
anyhow::bail!("Target image does not have {bootable_key} label");
}
}
let (commit_layer, component_layers, remaining_layers) =
parse_manifest_layout(&manifest, &config)?;
let query = |l: &Descriptor| query_layer(&self.repo, l.clone());
let commit_layer = query(commit_layer)?;
let component_layers = component_layers
.into_iter()
.map(query)
.collect::<Result<Vec<_>>>()?;
let remaining_layers = remaining_layers
.into_iter()
.map(query)
.collect::<Result<Vec<_>>>()?;
let previous_manifest_digest = previous_state.as_ref().map(|s| s.manifest_digest.clone());
let imp = PreparedImport {
manifest,
manifest_digest,
config,
previous_state,
previous_manifest_digest,
previous_imageid,
ostree_layers: component_layers,
ostree_commit_layer: commit_layer,
layers: remaining_layers,
};
Ok(PrepareResult::Ready(Box::new(imp)))
}
#[context("Unencapsulating base")]
pub(crate) async fn unencapsulate_base(
&mut self,
import: &mut store::PreparedImport,
write_refs: bool,
) -> Result<()> {
tracing::debug!("Fetching base");
if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
&& skopeo::container_policy_is_default_insecure()?
{
return Err(anyhow!("containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"));
}
let remote = match &self.imgref.sigverify {
SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
None
}
};
let des_layers = self.proxy.get_layer_info(&self.proxy_img).await?;
for layer in import.ostree_layers.iter_mut() {
if layer.commit.is_some() {
continue;
}
if let Some(p) = self.layer_progress.as_ref() {
p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
.await?;
}
let (blob, driver) = fetch_layer_decompress(
&mut self.proxy,
&self.proxy_img,
&import.manifest,
&layer.layer,
self.layer_byte_progress.as_ref(),
des_layers.as_ref(),
self.imgref.imgref.transport,
)
.await?;
let repo = self.repo.clone();
let target_ref = layer.ostree_ref.clone();
let import_task =
crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
let txn = repo.auto_transaction(Some(cancellable))?;
let mut importer = crate::tar::Importer::new_for_object_set(&repo);
let blob = tokio_util::io::SyncIoBridge::new(blob);
let mut archive = tar::Archive::new(blob);
importer.import_objects(&mut archive, Some(cancellable))?;
let commit = if write_refs {
let commit = importer.finish_import_object_set()?;
repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
tracing::debug!("Wrote {} => {}", target_ref, commit);
Some(commit)
} else {
None
};
txn.commit(Some(cancellable))?;
Ok::<_, anyhow::Error>(commit)
})
.map_err(|e| e.context(format!("Layer {}", layer.digest())));
let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
layer.commit = commit;
if let Some(p) = self.layer_progress.as_ref() {
p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
.await?;
}
}
if import.ostree_commit_layer.commit.is_none() {
if let Some(p) = self.layer_progress.as_ref() {
p.send(ImportProgress::OstreeChunkStarted(
import.ostree_commit_layer.layer.clone(),
))
.await?;
}
let (blob, driver) = fetch_layer_decompress(
&mut self.proxy,
&self.proxy_img,
&import.manifest,
&import.ostree_commit_layer.layer,
self.layer_byte_progress.as_ref(),
des_layers.as_ref(),
self.imgref.imgref.transport,
)
.await?;
let repo = self.repo.clone();
let target_ref = import.ostree_commit_layer.ostree_ref.clone();
let import_task =
crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
let txn = repo.auto_transaction(Some(cancellable))?;
let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
let blob = tokio_util::io::SyncIoBridge::new(blob);
let mut archive = tar::Archive::new(blob);
importer.import_commit(&mut archive, Some(cancellable))?;
let commit = importer.finish_import_commit();
if write_refs {
repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
tracing::debug!("Wrote {} => {}", target_ref, commit);
}
repo.mark_commit_partial(&commit, false)?;
txn.commit(Some(cancellable))?;
Ok::<_, anyhow::Error>(commit)
});
let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
import.ostree_commit_layer.commit = Some(commit);
if let Some(p) = self.layer_progress.as_ref() {
p.send(ImportProgress::OstreeChunkCompleted(
import.ostree_commit_layer.layer.clone(),
))
.await?;
}
};
Ok(())
}
pub async fn unencapsulate(mut self) -> Result<Import> {
let mut prep = match self.prepare_internal(false).await? {
PrepareResult::AlreadyPresent(_) => {
panic!("Should not have image present for unencapsulation")
}
PrepareResult::Ready(r) => r,
};
if !prep.layers.is_empty() {
anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
}
let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
self.unencapsulate_base(&mut prep, false).await?;
self.proxy.close_image(&self.proxy_img).await?;
let ostree_commit = prep.ostree_commit_layer.commit.unwrap();
let image_digest = prep.manifest_digest;
Ok(Import {
ostree_commit,
image_digest,
deprecated_warning,
})
}
#[context("Importing")]
pub async fn import(
mut self,
mut import: Box<PreparedImport>,
) -> Result<Box<LayeredImageState>> {
if let Some(status) = import.format_layer_status() {
system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
}
self.unencapsulate_base(&mut import, true).await?;
let des_layers = self.proxy.get_layer_info(&self.proxy_img).await?;
let mut proxy = self.proxy;
let proxy_img = self.proxy_img;
let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
let base_commit = import.ostree_commit_layer.commit.clone().unwrap();
let ostree_ref = ref_for_image(&target_imgref.imgref)?;
let mut layer_commits = Vec::new();
let mut layer_filtered_content: MetaFilteredData = HashMap::new();
for layer in import.layers {
if let Some(c) = layer.commit {
tracing::debug!("Reusing fetched commit {}", c);
layer_commits.push(c.to_string());
} else {
if let Some(p) = self.layer_progress.as_ref() {
p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
.await?;
}
let (blob, driver) = super::unencapsulate::fetch_layer_decompress(
&mut proxy,
&proxy_img,
&import.manifest,
&layer.layer,
self.layer_byte_progress.as_ref(),
des_layers.as_ref(),
self.imgref.imgref.transport,
)
.await?;
let opts = crate::tar::WriteTarOptions {
base: Some(base_commit.clone()),
selinux: true,
};
let r =
crate::tar::write_tar(&self.repo, blob, layer.ostree_ref.as_str(), Some(opts));
let r = super::unencapsulate::join_fetch(r, driver)
.await
.with_context(|| format!("Parsing layer blob {}", layer.digest()))?;
layer_commits.push(r.commit);
if !r.filtered.is_empty() {
let filtered = HashMap::from_iter(r.filtered.into_iter());
layer_filtered_content.insert(layer.digest().to_string(), filtered);
}
if let Some(p) = self.layer_progress.as_ref() {
p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
.await?;
}
}
}
proxy.close_image(&proxy_img).await?;
proxy.finalize().await?;
tracing::debug!("finalized proxy");
let serialized_manifest = serde_json::to_string(&import.manifest)?;
let serialized_config = serde_json::to_string(&import.config)?;
let mut metadata = HashMap::new();
metadata.insert(META_MANIFEST_DIGEST, import.manifest_digest.to_variant());
metadata.insert(META_MANIFEST, serialized_manifest.to_variant());
metadata.insert(META_CONFIG, serialized_config.to_variant());
metadata.insert(
"ostree.importer.version",
env!("CARGO_PKG_VERSION").to_variant(),
);
let filtered = layer_filtered_content.to_variant();
metadata.insert(META_FILTERED, filtered);
let metadata = metadata.to_variant();
let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
.unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
let repo = self.repo;
let state = crate::tokio_util::spawn_blocking_cancellable_flatten(
move |cancellable| -> Result<Box<LayeredImageState>> {
use rustix::fd::AsRawFd;
let cancellable = Some(cancellable);
let repo = &repo;
let txn = repo.auto_transaction(cancellable)?;
let devino = ostree::RepoDevInoCache::new();
let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
let repo_tmp = repodir.open_dir("tmp")?;
let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
let rootpath = "root";
let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
ostree::RepoCheckoutMode::None
} else {
ostree::RepoCheckoutMode::User
};
let mut checkout_opts = ostree::RepoCheckoutAtOptions {
mode: checkout_mode,
overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
devino_to_csum_cache: Some(devino.clone()),
no_copy_fallback: true,
force_copy_zerosized: true,
process_whiteouts: false,
..Default::default()
};
repo.checkout_at(
Some(&checkout_opts),
(*td).as_raw_fd(),
rootpath,
&base_commit,
cancellable,
)
.context("Checking out base commit")?;
checkout_opts.process_whiteouts = true;
for commit in layer_commits {
repo.checkout_at(
Some(&checkout_opts),
(*td).as_raw_fd(),
rootpath,
&commit,
cancellable,
)
.with_context(|| format!("Checking out layer {commit}"))?;
}
let modifier =
ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::CONSUME, None);
modifier.set_devino_cache(&devino);
let mt = ostree::MutableTree::new();
repo.write_dfd_to_mtree(
(*td).as_raw_fd(),
rootpath,
&mt,
Some(&modifier),
cancellable,
)
.context("Writing merged filesystem to mtree")?;
let merged_root = repo
.write_mtree(&mt, cancellable)
.context("Writing mtree")?;
let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
let merged_commit = repo
.write_commit_with_time(
None,
None,
None,
Some(&metadata),
&merged_root,
timestamp,
cancellable,
)
.context("Writing commit")?;
if !self.no_imgref {
repo.transaction_set_ref(None, &ostree_ref, Some(merged_commit.as_str()));
}
txn.commit(cancellable)?;
if !self.disable_gc {
let n: u32 = gc_image_layers_impl(repo, cancellable)?;
tracing::debug!("pruned {n} layers");
}
let state = query_image_commit(repo, &merged_commit)?;
Ok(state)
},
)
.await?;
Ok(state)
}
}
pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
let cancellable = gio::Cancellable::NONE;
let refs = repo.list_refs_ext(
Some(IMAGE_PREFIX),
ostree::RepoListRefsExtFlags::empty(),
cancellable,
)?;
refs.keys()
.map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
.collect()
}
fn try_query_image_ref(
repo: &ostree::Repo,
imgref: &ImageReference,
) -> Result<Option<Box<LayeredImageState>>> {
let ostree_ref = &ref_for_image(imgref)?;
if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
match query_image_commit(repo, merge_rev.as_str()) {
Ok(r) => Ok(Some(r)),
Err(e) => {
eprintln!("error: failed to query image commit: {e}");
Ok(None)
}
}
} else {
Ok(None)
}
}
#[context("Querying image {imgref}")]
pub fn query_image_ref(
repo: &ostree::Repo,
imgref: &ImageReference,
) -> Result<Option<Box<LayeredImageState>>> {
let ostree_ref = &ref_for_image(imgref)?;
let merge_rev = repo.resolve_rev(ostree_ref, true)?;
merge_rev
.map(|r| query_image_commit(repo, r.as_str()))
.transpose()
}
pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
let merge_commit = commit.to_string();
let merge_commit_obj = repo.load_commit(commit)?.0;
let commit_meta = &merge_commit_obj.child_value(0);
let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
let configuration = image_config_from_commitmeta(commit_meta)?;
let mut layers = manifest.layers().iter().cloned();
let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
let base_layer = query_layer(repo, base_layer)?;
let ostree_ref = base_layer.ostree_ref.as_str();
let base_commit = base_layer
.commit
.ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
let is_layered = layers.count() > 0;
let state = Box::new(LayeredImageState {
base_commit,
merge_commit,
is_layered,
manifest_digest,
manifest,
configuration,
});
tracing::debug!(state = ?state);
Ok(state)
}
pub fn query_image(
repo: &ostree::Repo,
imgref: &OstreeImageReference,
) -> Result<Option<Box<LayeredImageState>>> {
query_image_ref(repo, &imgref.imgref)
}
fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
let ostree_ref = ref_for_image(imgref)?;
let rev = repo.require_rev(&ostree_ref)?;
let (commit_obj, _) = repo.load_commit(rev.as_str())?;
let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
Ok(manifest_data_from_commitmeta(commit_meta)?.0)
}
#[context("Copying image")]
#[deprecated = "Use copy_as instead"]
pub async fn copy(
src_repo: &ostree::Repo,
dest_repo: &ostree::Repo,
imgref: &OstreeImageReference,
) -> Result<()> {
let imgref = &imgref.imgref;
copy_as(src_repo, imgref, dest_repo, imgref).await
}
#[context("Copying image")]
pub async fn copy_as(
src_repo: &ostree::Repo,
src_imgref: &ImageReference,
dest_repo: &ostree::Repo,
dest_imgref: &ImageReference,
) -> Result<()> {
let src_ostree_ref = ref_for_image(src_imgref)?;
let src_commit = src_repo.require_rev(&src_ostree_ref)?;
let manifest = manifest_for_image(src_repo, src_imgref)?;
let layer_refs = manifest
.layers()
.iter()
.map(ref_for_layer)
.chain(std::iter::once(Ok(src_commit.to_string())));
for ostree_ref in layer_refs {
let ostree_ref = ostree_ref?;
let src_repo = src_repo.clone();
let dest_repo = dest_repo.clone();
crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
let cancellable = Some(cancellable);
let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
let flags = ostree::RepoPullFlags::MIRROR;
let opts = glib::VariantDict::new(None);
let refs = [ostree_ref.as_str()];
opts.insert("disable-verify-bindings", &true);
opts.insert("refs", &&refs[..]);
opts.insert("flags", &(flags.bits() as i32));
let options = opts.to_variant();
dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
Ok(())
})
.await?;
}
let dest_ostree_ref = ref_for_image(dest_imgref)?;
dest_repo.set_ref_immediate(
None,
&dest_ostree_ref,
Some(&src_commit),
gio::Cancellable::NONE,
)?;
Ok(())
}
#[context("Listing deployment manifests")]
fn list_container_deployment_manifests(
repo: &ostree::Repo,
cancellable: Option<&gio::Cancellable>,
) -> Result<Vec<ImageManifest>> {
let commits = OSTREE_BASE_DEPLOYMENT_REFS
.iter()
.chain(RPMOSTREE_BASE_REFS)
.chain(std::iter::once(&BASE_IMAGE_PREFIX))
.try_fold(
std::collections::HashSet::new(),
|mut acc, &p| -> Result<_> {
let refs = repo.list_refs_ext(
Some(p),
ostree::RepoListRefsExtFlags::empty(),
cancellable,
)?;
for (_, v) in refs {
acc.insert(v);
}
Ok(acc)
},
)?;
let mut r = Vec::new();
for commit in commits {
let commit_obj = repo.load_commit(&commit)?.0;
let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
if commit_meta
.lookup::<String>(META_MANIFEST_DIGEST)?
.is_some()
{
tracing::trace!("Commit {commit} is a container image");
let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
r.push(manifest);
}
}
Ok(r)
}
pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
gc_image_layers_impl(repo, gio::Cancellable::NONE)
}
#[context("Pruning image layers")]
fn gc_image_layers_impl(
repo: &ostree::Repo,
cancellable: Option<&gio::Cancellable>,
) -> Result<u32> {
let all_images = list_images(repo)?;
let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
let all_manifests = all_images
.into_iter()
.map(|img| {
ImageReference::try_from(img.as_str()).and_then(|ir| manifest_for_image(repo, &ir))
})
.chain(deployment_commits.into_iter().map(Ok))
.collect::<Result<Vec<_>>>()?;
tracing::debug!("Images found: {}", all_manifests.len());
let mut referenced_layers = BTreeSet::new();
for m in all_manifests.iter() {
for layer in m.layers() {
referenced_layers.insert(layer.digest().as_str());
}
}
tracing::debug!("Referenced layers: {}", referenced_layers.len());
let found_layers = repo
.list_refs_ext(
Some(LAYER_PREFIX),
ostree::RepoListRefsExtFlags::empty(),
cancellable,
)?
.into_iter()
.map(|v| v.0);
tracing::debug!("Found layers: {}", found_layers.len());
let mut pruned = 0u32;
for layer_ref in found_layers {
let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
if referenced_layers.remove(layer_digest.as_str()) {
continue;
}
pruned += 1;
tracing::debug!("Pruning: {}", layer_ref.as_str());
repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
}
Ok(pruned)
}
#[cfg(feature = "internal-testing-api")]
pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
let cancellable = gio::Cancellable::NONE;
let n = repo
.list_refs_ext(
Some(LAYER_PREFIX),
ostree::RepoListRefsExtFlags::empty(),
cancellable,
)?
.len();
Ok(n as u32)
}
pub fn image_filtered_content_warning(
repo: &ostree::Repo,
image: &ImageReference,
) -> Result<Option<String>> {
use std::fmt::Write;
let ostree_ref = ref_for_image(image)?;
let rev = repo.require_rev(&ostree_ref)?;
let commit_obj = repo.load_commit(rev.as_str())?.0;
let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
let r = commit_meta
.lookup::<MetaFilteredData>(META_FILTERED)?
.filter(|v| !v.is_empty())
.map(|v| {
let mut filtered = HashMap::<&String, u32>::new();
for paths in v.values() {
for (k, v) in paths {
let e = filtered.entry(k).or_default();
*e += v;
}
}
let mut buf = "Image contains non-ostree compatible file paths:".to_string();
for (k, v) in filtered {
write!(buf, " {k}: {v}").unwrap();
}
buf
});
Ok(r)
}
#[context("Pruning {img}")]
pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
let ostree_ref = &ref_for_image(img)?;
let found = repo.resolve_rev(ostree_ref, true)?.is_some();
if found {
repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
}
Ok(found)
}
pub fn remove_images<'a>(
repo: &ostree::Repo,
imgs: impl IntoIterator<Item = &'a ImageReference>,
) -> Result<()> {
let mut missing = Vec::new();
for img in imgs.into_iter() {
let found = remove_image(repo, img)?;
if !found {
missing.push(img);
}
}
if !missing.is_empty() {
let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
a.push_str(&v.to_string());
a
});
return Err(anyhow::anyhow!("Missing images: {missing}"));
}
Ok(())
}
#[derive(Debug, Default)]
struct CompareState {
verified: BTreeSet<Utf8PathBuf>,
inode_corrupted: BTreeSet<Utf8PathBuf>,
unknown_corrupted: BTreeSet<Utf8PathBuf>,
}
impl CompareState {
fn is_ok(&self) -> bool {
self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
}
}
fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
if src.file_type() != target.file_type() {
return false;
}
if src.size() != target.size() {
return false;
}
for attr in ["unix::uid", "unix::gid", "unix::mode"] {
if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
return false;
}
}
true
}
#[context("Querying object inode")]
fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
let (prefix, suffix) = checksum.split_at(2);
let objpath = format!("objects/{}/{}.file", prefix, suffix);
let metadata = repodir.symlink_metadata(objpath)?;
Ok(metadata.ino())
}
fn compare_commit_trees(
repo: &ostree::Repo,
root: &Utf8Path,
target: &ostree::RepoFile,
expected: &ostree::RepoFile,
exact: bool,
colliding_inodes: &BTreeSet<u64>,
state: &mut CompareState,
) -> Result<()> {
let cancellable = gio::Cancellable::NONE;
let queryattrs = "standard::name,standard::type";
let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
while let Some(expected_info) = expected_iter.next_file(cancellable)? {
let expected_child = expected_iter.child(&expected_info);
let name = expected_info.name();
let name = name.to_str().expect("UTF-8 ostree name");
let path = Utf8PathBuf::from(format!("{root}{name}"));
let target_child = target.child(name);
let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
.context("querying optional to")?;
let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
if let Some(target_info) = target_info {
let to_child = target_child
.downcast::<ostree::RepoFile>()
.expect("downcast");
to_child.ensure_resolved()?;
let from_child = expected_child
.downcast::<ostree::RepoFile>()
.expect("downcast");
from_child.ensure_resolved()?;
if is_dir {
let from_contents_checksum = from_child.tree_get_contents_checksum();
let to_contents_checksum = to_child.tree_get_contents_checksum();
if from_contents_checksum != to_contents_checksum {
let subpath = Utf8PathBuf::from(format!("{}/", path));
compare_commit_trees(
repo,
&subpath,
&from_child,
&to_child,
exact,
colliding_inodes,
state,
)?;
}
} else {
let from_checksum = from_child.checksum();
let to_checksum = to_child.checksum();
let matches = if exact {
from_checksum == to_checksum
} else {
compare_file_info(&target_info, &expected_info)
};
if !matches {
let from_inode = inode_of_object(repo, &from_checksum)?;
let to_inode = inode_of_object(repo, &to_checksum)?;
if colliding_inodes.contains(&from_inode)
|| colliding_inodes.contains(&to_inode)
{
state.inode_corrupted.insert(path);
} else {
state.unknown_corrupted.insert(path);
}
} else {
state.verified.insert(path);
}
}
} else {
eprintln!("Missing {path}");
state.unknown_corrupted.insert(path);
}
}
Ok(())
}
#[context("Verifying container image state")]
pub(crate) fn verify_container_image(
sysroot: &SysrootLock,
imgref: &ImageReference,
state: &LayeredImageState,
colliding_inodes: &BTreeSet<u64>,
verbose: bool,
) -> Result<bool> {
let cancellable = gio::Cancellable::NONE;
let repo = &sysroot.repo();
let merge_commit = state.merge_commit.as_str();
let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
let merge_commit_root = merge_commit_root
.downcast::<ostree::RepoFile>()
.expect("downcast");
merge_commit_root.ensure_resolved()?;
let config = state
.configuration
.as_ref()
.ok_or_else(|| anyhow!("Missing configuration for image"))?;
let (commit_layer, _component_layers, remaining_layers) =
parse_manifest_layout(&state.manifest, config)?;
let mut comparison_state = CompareState::default();
let query = |l: &Descriptor| query_layer(repo, l.clone());
let base_tree = repo
.read_commit(&state.base_commit, cancellable)?
.0
.downcast::<ostree::RepoFile>()
.expect("downcast");
println!(
"Verifying with base ostree layer {}",
ref_for_layer(commit_layer)?
);
compare_commit_trees(
repo,
"/".into(),
&merge_commit_root,
&base_tree,
true,
colliding_inodes,
&mut comparison_state,
)?;
let remaining_layers = remaining_layers
.into_iter()
.map(query)
.collect::<Result<Vec<_>>>()?;
println!("Image has {} derived layers", remaining_layers.len());
for layer in remaining_layers.iter().rev() {
let layer_ref = layer.ostree_ref.as_str();
let layer_commit = layer
.commit
.as_deref()
.ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
let layer_tree = repo
.read_commit(layer_commit, cancellable)?
.0
.downcast::<ostree::RepoFile>()
.expect("downcast");
compare_commit_trees(
repo,
"/".into(),
&merge_commit_root,
&layer_tree,
false,
colliding_inodes,
&mut comparison_state,
)?;
}
let n_verified = comparison_state.verified.len();
if comparison_state.is_ok() {
println!("OK image {imgref} (verified={n_verified})");
println!();
} else {
let n_inode = comparison_state.inode_corrupted.len();
let n_other = comparison_state.unknown_corrupted.len();
eprintln!("warning: Found corrupted merge commit");
eprintln!(" inode clashes: {n_inode}");
eprintln!(" unknown: {n_other}");
eprintln!(" ok: {n_verified}");
if verbose {
eprintln!("Mismatches:");
for path in comparison_state.inode_corrupted {
eprintln!(" inode: {path}");
}
for path in comparison_state.unknown_corrupted {
eprintln!(" other: {path}");
}
}
eprintln!();
return Ok(false);
}
Ok(true)
}