1use std::collections::HashSet;
48use std::io::{BufRead, Write};
49use std::os::fd::{AsFd, AsRawFd};
50use std::process::Command;
51use std::sync::Arc;
52use std::time::Duration;
53
54use anyhow::{Context, Result, anyhow};
55use bootc_utils::skopeo_bin;
56use cap_std::fs::{Dir, MetadataExt};
57use cap_std_ext::cap_std;
58use cap_std_ext::dirext::CapStdExtDirExt;
59use fn_error_context::context;
60use linux_kernel_cmdline::utf8::CmdlineOwned;
61use ostree::{gio, glib};
62use ostree_container::OstreeImageReference;
63use ostree_ext::container as ostree_container;
64use ostree_ext::container::store::{ImageImporter, ImportProgress, PrepareResult, PreparedImport};
65use ostree_ext::oci_spec::image::{Descriptor, Digest};
66use ostree_ext::ostree::Deployment;
67use ostree_ext::ostree::{self, Sysroot};
68use ostree_ext::sysroot::SysrootLock;
69use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten;
70
71use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep};
72use crate::spec::ImageReference;
73use crate::spec::{BootOrder, HostSpec};
74use crate::status::labels_of_config;
75use crate::store::Storage;
76use crate::utils::async_task_with_spinner;
77
78const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc";
80
81const PULL_MAX_RETRIES: u32 = 3;
87const PULL_RETRY_INITIAL_DELAY: Duration = Duration::from_secs(2);
88
89#[derive(Clone)]
91pub(crate) struct PullProgress {
92 bars: indicatif::MultiProgress,
93 json: ProgressWriter,
94 quiet: bool,
95}
96
97impl PullProgress {
98 pub(crate) fn new(quiet: bool, json: ProgressWriter) -> Self {
99 let bars = indicatif::MultiProgress::new();
100 if quiet {
101 bars.set_draw_target(indicatif::ProgressDrawTarget::hidden());
102 }
103 Self { bars, json, quiet }
104 }
105
106 fn println(&self, message: impl AsRef<str>) {
107 if self.quiet {
108 return;
109 }
110 if let Err(error) = self.bars.println(message) {
111 tracing::debug!(%error, "Writing pull progress message");
112 }
113 }
114
115 async fn send(&self, event: Event<'_>) {
116 self.json.send(event.clone()).await;
117 if let Event::ProgressSteps { description, .. } = &event {
118 self.println(description);
119 }
120 }
121
122 fn clear(&self) {
123 if let Err(error) = self.bars.clear() {
124 tracing::debug!(%error, "Clearing pull progress");
125 }
126 }
127}
128
129pub(crate) fn new_proxy_config() -> ostree_ext::containers_image_proxy::ImageProxyConfig {
134 let mut c = ostree_ext::containers_image_proxy::ImageProxyConfig::default();
135 c.user_agent_prefix = Some(format!("bootc/{}", env!("CARGO_PKG_VERSION")));
136 c
137}
138
139const BOOTC_DERIVED_KEY: &str = "bootc.derived";
141
142pub(crate) struct RequiredHostSpec<'a> {
144 pub(crate) image: &'a ImageReference,
145}
146
147pub(crate) struct ImageState {
149 pub(crate) manifest_digest: Digest,
150 pub(crate) version: Option<String>,
151 pub(crate) ostree_commit: String,
152}
153
154impl<'a> RequiredHostSpec<'a> {
155 pub(crate) fn from_spec(spec: &'a HostSpec) -> Result<Self> {
158 let image = spec
159 .image
160 .as_ref()
161 .ok_or_else(|| anyhow::anyhow!("Missing image in specification"))?;
162 Ok(Self { image })
163 }
164}
165
166impl From<ostree_container::store::LayeredImageState> for ImageState {
167 fn from(value: ostree_container::store::LayeredImageState) -> Self {
168 let version = value.version().map(|v| v.to_owned());
169 let ostree_commit = value.get_commit().to_owned();
170 Self {
171 manifest_digest: value.manifest_digest,
172 version,
173 ostree_commit,
174 }
175 }
176}
177
178impl ImageState {
179 pub(crate) fn get_manifest(
181 &self,
182 repo: &ostree::Repo,
183 ) -> Result<Option<ostree_ext::oci_spec::image::ImageManifest>> {
184 ostree_container::store::query_image_commit(repo, &self.ostree_commit)
185 .map(|v| Some(v.manifest))
186 }
187}
188
189fn configure_importer(
191 imp: &mut ostree_container::store::ImageImporter,
192 booted_deployment: Option<&ostree::Deployment>,
193) {
194 imp.require_bootable();
195 imp.disable_gc();
197 if let Some(deployment) = booted_deployment {
198 imp.set_sepolicy_commit(deployment.csum().to_string());
199 }
200}
201
202pub(crate) async fn new_importer(
204 repo: &ostree::Repo,
205 imgref: &ostree_container::OstreeImageReference,
206 booted_deployment: Option<&ostree::Deployment>,
207) -> Result<ostree_container::store::ImageImporter> {
208 new_importer_with_config(repo, imgref, new_proxy_config(), booted_deployment).await
209}
210
211pub(crate) async fn new_importer_with_config(
213 repo: &ostree::Repo,
214 imgref: &ostree_container::OstreeImageReference,
215 config: ostree_ext::containers_image_proxy::ImageProxyConfig,
216 booted_deployment: Option<&ostree::Deployment>,
217) -> Result<ostree_container::store::ImageImporter> {
218 let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?;
219 configure_importer(&mut imp, booted_deployment);
220 Ok(imp)
221}
222
223pub(crate) fn check_bootc_label(config: &ostree_ext::oci_spec::image::ImageConfiguration) {
224 if let Some(label) =
225 labels_of_config(config).and_then(|labels| labels.get(crate::metadata::BOOTC_COMPAT_LABEL))
226 {
227 match label.as_str() {
228 crate::metadata::COMPAT_LABEL_V1 => {}
229 o => crate::journal::journal_print(
230 libsystemd::logging::Priority::Warning,
231 &format!(
232 "notice: Unknown {} value {}",
233 crate::metadata::BOOTC_COMPAT_LABEL,
234 o
235 ),
236 ),
237 }
238 } else {
239 crate::journal::journal_print(
240 libsystemd::logging::Priority::Warning,
241 &format!(
242 "notice: Image is missing label: {}",
243 crate::metadata::BOOTC_COMPAT_LABEL
244 ),
245 )
246 }
247}
248
249fn descriptor_of_progress(p: &ImportProgress) -> &Descriptor {
250 match p {
251 ImportProgress::OstreeChunkStarted(l) => l,
252 ImportProgress::OstreeChunkCompleted(l) => l,
253 ImportProgress::DerivedLayerStarted(l) => l,
254 ImportProgress::DerivedLayerCompleted(l) => l,
255 }
256}
257
258fn prefix_of_progress(p: &ImportProgress) -> &'static str {
259 match p {
260 ImportProgress::OstreeChunkStarted(_) | ImportProgress::OstreeChunkCompleted(_) => {
261 "ostree chunk"
262 }
263 ImportProgress::DerivedLayerStarted(_) | ImportProgress::DerivedLayerCompleted(_) => {
264 "layer"
265 }
266 }
267}
268
269struct LayerProgressConfig {
271 layers: tokio::sync::mpsc::Receiver<ostree_container::store::ImportProgress>,
272 layer_bytes: tokio::sync::watch::Receiver<Option<ostree_container::store::LayerProgress>>,
273 digest: Box<str>,
274 n_layers_to_fetch: usize,
275 layers_total: usize,
276 bytes_to_download: u64,
277 bytes_total: u64,
278 progress: PullProgress,
279}
280
281async fn handle_layer_progress_print(mut config: LayerProgressConfig) {
283 let start = std::time::Instant::now();
284 let mut total_read = 0u64;
285 let bar = &config.progress.bars;
286 let layers_bar = bar.add(indicatif::ProgressBar::new(
287 config.n_layers_to_fetch.try_into().unwrap(),
288 ));
289 let byte_bar = bar.add(indicatif::ProgressBar::new(0));
290 layers_bar.set_style(
293 indicatif::ProgressStyle::default_bar()
294 .template("{prefix} {bar} {pos}/{len} {wide_msg}")
295 .unwrap(),
296 );
297 let taskname = "Fetching layers";
298 layers_bar.set_prefix(taskname);
299 layers_bar.set_message("");
300 byte_bar.set_prefix("Fetching");
301 byte_bar.set_style(
302 indicatif::ProgressStyle::default_bar()
303 .template(
304 " └ {prefix} {bar} {binary_bytes}/{binary_total_bytes} ({binary_bytes_per_sec}) {wide_msg}",
305 )
306 .unwrap()
307 );
308
309 let mut subtasks = vec![];
310 let mut subtask: SubTaskBytes = Default::default();
311 loop {
312 tokio::select! {
313 biased;
315 layer = config.layers.recv() => {
316 if let Some(l) = layer {
317 let layer = descriptor_of_progress(&l);
318 let layer_type = prefix_of_progress(&l);
319 let short_digest = &layer.digest().digest()[0..21];
320 let layer_size = layer.size();
321 if l.is_starting() {
322 byte_bar.reset_elapsed();
324 byte_bar.reset_eta();
325 byte_bar.set_length(layer_size);
326 byte_bar.set_message(format!("{layer_type} {short_digest}"));
327
328 subtask = SubTaskBytes {
329 subtask: layer_type.into(),
330 description: format!("{layer_type}: {short_digest}").clone().into(),
331 id: short_digest.to_string().clone().into(),
332 bytes_cached: 0,
333 bytes: 0,
334 bytes_total: layer_size,
335 };
336 } else {
337 let actual_size = byte_bar.length().unwrap_or(layer_size);
340 byte_bar.set_position(actual_size);
341 layers_bar.inc(1);
342 total_read = total_read.saturating_add(actual_size);
343 subtask.bytes_total = actual_size;
345 subtask.bytes = actual_size;
346 subtasks.push(subtask.clone());
347 config.progress.json.send(Event::ProgressBytes {
348 task: "pulling".into(),
349 description: format!("Pulling Image: {}", config.digest).into(),
350 id: (*config.digest).into(),
351 bytes_cached: config.bytes_total - config.bytes_to_download,
352 bytes: total_read,
353 bytes_total: config.bytes_to_download,
354 steps_cached: (config.layers_total - config.n_layers_to_fetch) as u64,
355 steps: layers_bar.position(),
356 steps_total: config.n_layers_to_fetch as u64,
357 subtasks: subtasks.clone(),
358 }).await;
359 }
360 } else {
361 break
363 };
364 },
365 r = config.layer_bytes.changed() => {
366 if r.is_err() {
367 break
369 }
370 let bytes = {
371 let bytes = config.layer_bytes.borrow_and_update();
372 bytes.as_ref().cloned()
373 };
374 if let Some(bytes) = bytes {
375 byte_bar.set_length(bytes.total);
379 byte_bar.set_position(bytes.fetched);
380 subtask.bytes_total = bytes.total;
381 subtask.bytes = byte_bar.position();
382 config.progress.json.send_lossy(Event::ProgressBytes {
383 task: "pulling".into(),
384 description: format!("Pulling Image: {}", config.digest).into(),
385 id: (*config.digest).into(),
386 bytes_cached: config.bytes_total - config.bytes_to_download,
387 bytes: total_read + byte_bar.position(),
388 bytes_total: config.bytes_to_download,
389 steps_cached: (config.layers_total - config.n_layers_to_fetch) as u64,
390 steps: layers_bar.position(),
391 steps_total: config.n_layers_to_fetch as u64,
392 subtasks: subtasks.clone().into_iter().chain([subtask.clone()]).collect(),
393 }).await;
394 }
395 }
396 }
397 }
398 byte_bar.finish_and_clear();
399 layers_bar.finish_and_clear();
400 bar.remove(&byte_bar);
401 bar.remove(&layers_bar);
402 config.progress.clear();
403 let end = std::time::Instant::now();
404 let elapsed = end.duration_since(start);
405 let persec = total_read as f64 / elapsed.as_secs_f64();
406 let persec = indicatif::HumanBytes(persec as u64);
407 config.progress.println(format!(
408 "Fetched layers: {} in {} ({}/s)",
409 indicatif::HumanBytes(total_read),
410 indicatif::HumanDuration(elapsed),
411 persec,
412 ));
413
414 config
418 .progress
419 .json
420 .send(Event::ProgressSteps {
421 task: "importing".into(),
422 description: "Importing Image".into(),
423 id: (*config.digest).into(),
424 steps_cached: 0,
425 steps: 0,
426 steps_total: 1,
427 subtasks: [SubTaskStep {
428 subtask: "importing".into(),
429 description: "Importing Image".into(),
430 id: "importing".into(),
431 completed: false,
432 }]
433 .into(),
434 })
435 .await;
436}
437
438pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> {
441 let ostree = sysroot.get_ostree()?;
442 let deployments = ostree.deployments();
443 let mut all_bound_images = Vec::new();
444 for deployment in deployments {
445 let bound = crate::boundimage::query_bound_images_for_deployment(ostree, &deployment)?;
446 all_bound_images.extend(bound.into_iter());
447 if let Some(host_image) = crate::status::boot_entry_from_deployment(ostree, &deployment)?
451 .image
452 .map(|i| i.image)
453 {
454 all_bound_images.push(crate::boundimage::BoundImage {
455 image: host_image.image.clone(),
456 auth_file: None,
457 });
458 }
459 }
460 let image_names = HashSet::from_iter(all_bound_images.iter().map(|img| img.image.as_str()));
462 let pruned = sysroot
463 .get_ensure_imgstore()?
464 .prune_except_roots(&image_names)
465 .await?;
466 tracing::debug!("Pruned images: {}", pruned.len());
467 Ok(())
468}
469
470fn check_disk_space_inner(
473 fd: impl AsFd,
474 bytes_to_fetch: u64,
475 min_free: u64,
476 imgref: &ImageReference,
477) -> Result<()> {
478 let stat = rustix::fs::fstatvfs(fd)?;
479 let bytes_avail = stat.f_bsize.checked_mul(stat.f_bavail).unwrap_or(u64::MAX);
480 let usable = bytes_avail.saturating_sub(min_free);
481 tracing::trace!("bytes_avail: {bytes_avail} min_free: {min_free} usable: {usable}");
482
483 if bytes_to_fetch > usable {
484 anyhow::bail!(
485 "Insufficient free space for {image} (available: {available} required: {required})",
486 available = ostree_ext::glib::format_size(usable),
487 required = ostree_ext::glib::format_size(bytes_to_fetch),
488 image = imgref.image,
489 );
490 }
491 Ok(())
492}
493
494pub(crate) fn check_disk_space_ostree(
497 repo: &ostree::Repo,
498 image_meta: &PreparedImportMeta,
499 imgref: &ImageReference,
500) -> Result<()> {
501 let min_free = repo.min_free_space_bytes().unwrap_or(0);
502 check_disk_space_inner(
503 repo.dfd_borrow(),
504 image_meta.bytes_to_fetch,
505 min_free,
506 imgref,
507 )
508}
509
510pub(crate) fn check_disk_space_unified(
513 cfs: &crate::store::ComposefsRepository,
514 image_meta: &PreparedImportMeta,
515 imgref: &ImageReference,
516) -> Result<()> {
517 check_disk_space_inner(cfs.objects_dir()?, image_meta.bytes_to_fetch, 0, imgref)
518}
519
520pub(crate) fn check_disk_space_composefs(
523 cfs: &crate::store::ComposefsRepository,
524 manifest: &ostree_ext::oci_spec::image::ImageManifest,
525 imgref: &ImageReference,
526) -> Result<()> {
527 let bytes_to_fetch: u64 = manifest
528 .layers()
529 .iter()
530 .map(|l: &ostree_ext::oci_spec::image::Descriptor| l.size())
531 .sum();
532 check_disk_space_inner(cfs.objects_dir()?, bytes_to_fetch, 0, imgref)
533}
534
535pub(crate) struct PreparedImportMeta {
536 pub imp: ImageImporter,
537 pub prep: Box<PreparedImport>,
538 pub digest: Digest,
539 pub n_layers_to_fetch: usize,
540 pub layers_total: usize,
541 pub bytes_to_fetch: u64,
542 pub bytes_total: u64,
543}
544
545pub(crate) enum PreparedPullResult {
546 Ready(Box<PreparedImportMeta>),
547 AlreadyPresent(Box<ImageState>),
548}
549
550pub(crate) async fn prepare_for_pull(
551 repo: &ostree::Repo,
552 imgref: &ImageReference,
553 target_imgref: Option<&OstreeImageReference>,
554 booted_deployment: Option<&ostree::Deployment>,
555) -> Result<PreparedPullResult> {
556 let imgref_canonicalized = imgref.clone().canonicalize()?;
557 tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
558 let ostree_imgref = &OstreeImageReference::from(imgref_canonicalized);
559 let mut imp = new_importer(repo, ostree_imgref, booted_deployment).await?;
560 if let Some(target) = target_imgref {
561 imp.set_target(target);
562 }
563 let prep = match imp.prepare().await? {
564 PrepareResult::AlreadyPresent(c) => {
565 println!("No changes in {imgref:#} => {}", c.manifest_digest);
566 return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into())));
567 }
568 PrepareResult::Ready(p) => p,
569 };
570 Ok(PreparedPullResult::Ready(Box::new(
571 prepared_import_meta(imp, prep).await?,
572 )))
573}
574
575async fn prepared_import_meta(
578 imp: ImageImporter,
579 prep: Box<PreparedImport>,
580) -> Result<PreparedImportMeta> {
581 check_bootc_label(&prep.config);
582 if let Some(warning) = prep.deprecated_warning() {
583 ostree_ext::cli::print_deprecated_warning(warning).await;
584 }
585 ostree_ext::cli::print_layer_status(&prep);
586 let layers_to_fetch = prep.layers_to_fetch().collect::<Result<Vec<_>>>()?;
587
588 Ok(PreparedImportMeta {
589 imp,
590 n_layers_to_fetch: layers_to_fetch.len(),
591 layers_total: prep.all_layers().count(),
592 bytes_to_fetch: layers_to_fetch.iter().map(|(l, _)| l.layer.size()).sum(),
593 bytes_total: prep.all_layers().map(|l| l.layer.size()).sum(),
594 digest: prep.manifest_digest.clone(),
595 prep,
596 })
597}
598
599pub(crate) async fn prepare_for_pull_delta(
602 repo: &ostree::Repo,
603 progress: &PullProgress,
604 imgref: &ImageReference,
605 delta: Arc<crate::delta::Delta>,
606 booted_deployment: Option<&ostree::Deployment>,
607) -> Result<PreparedPullResult> {
608 delta.validate_image_reference(imgref)?;
609 let imgref_canonicalized = imgref.clone().canonicalize()?;
610 tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
611 let ostree_imgref = &OstreeImageReference::from(imgref_canonicalized);
612
613 let mut imp = ImageImporter::new_without_proxy(repo, ostree_imgref)?;
614 configure_importer(&mut imp, booted_deployment);
615
616 let layer_source = Box::new(crate::delta_ostree::DeltaLayerSource::new(
617 Arc::clone(&delta),
618 repo,
619 ));
620 let prep = match imp
621 .prepare_from_manifest(
622 delta.target_manifest_digest().clone(),
623 delta.target_manifest().clone(),
624 delta.target_config().clone(),
625 layer_source,
626 )
627 .await?
628 {
629 PrepareResult::AlreadyPresent(c) => {
630 progress.println(format!("No changes in {imgref:#} => {}", c.manifest_digest));
631 return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into())));
632 }
633 PrepareResult::Ready(p) => p,
634 };
635
636 Ok(PreparedPullResult::Ready(Box::new(
637 prepared_import_meta(imp, prep).await?,
638 )))
639}
640
641pub(crate) async fn pull_delta(
643 repo: &ostree::Repo,
644 imgref: &ImageReference,
645 delta: Arc<crate::delta::Delta>,
646 quiet: bool,
647 prog: ProgressWriter,
648 booted_deployment: Option<&ostree::Deployment>,
649) -> Result<Box<ImageState>> {
650 let progress = PullProgress::new(quiet, prog);
651 progress.println(format!("Applying delta {}", delta.describe()));
652 match prepare_for_pull_delta(repo, &progress, imgref, delta, booted_deployment).await? {
653 PreparedPullResult::AlreadyPresent(existing) => Ok(existing),
654 PreparedPullResult::Ready(prepared_image_meta) => {
655 check_disk_space_ostree(repo, &prepared_image_meta, imgref)?;
656 let r = pull_from_prepared(imgref, progress, *prepared_image_meta).await;
657 if r.is_err() {
658 if let Err(e) = ostree_container::store::gc_image_layers(repo) {
664 tracing::warn!("Pruning layers after failed delta apply: {e:#}");
665 }
666 }
667 r
668 }
669 }
670}
671
672pub(crate) async fn image_exists_in_unified_storage(
680 store: &Storage,
681 imgref: &ImageReference,
682) -> Result<bool> {
683 let imgstore = store.get_ensure_imgstore()?;
684 let image_ref_str = imgref.to_transport_image()?;
685 imgstore.exists(&image_ref_str).await
686}
687
688pub(crate) async fn prepare_for_pull_unified(
691 repo: &ostree::Repo,
692 imgref: &ImageReference,
693 target_imgref: Option<&OstreeImageReference>,
694 store: &Storage,
695 booted_deployment: Option<&ostree::Deployment>,
696) -> Result<PreparedPullResult> {
697 let imgstore = store.get_ensure_imgstore()?;
699
700 let image_ref_str = imgref.to_transport_image()?;
701
702 tracing::info!(
705 "Unified pull: pulling from transport '{}' to bootc storage",
706 &imgref.transport
707 );
708
709 imgstore
712 .pull_with_progress(&image_ref_str)
713 .await
714 .context("Pulling image into bootc containers-storage")?;
715
716 tracing::info!("Unified pull: now importing from containers-storage transport");
718 let containers_storage_imgref = ImageReference {
719 transport: "containers-storage".to_string(),
720 image: imgref.image.clone(),
721 signature: imgref.signature.clone(),
722 };
723 let ostree_imgref = OstreeImageReference::from(containers_storage_imgref);
724
725 let mut config = new_proxy_config();
727 let mut cmd = Command::new(skopeo_bin());
728 let storage_path = format!(
730 "{}/{}",
731 store.physical_root_path,
732 crate::podstorage::CStorage::subpath()
733 );
734 crate::podstorage::set_additional_image_store(&mut cmd, &storage_path);
735 config.skopeo_cmd = Some(cmd);
736
737 let mut imp = new_importer_with_config(repo, &ostree_imgref, config, booted_deployment).await?;
739 if let Some(target) = target_imgref {
740 imp.set_target(target);
741 }
742 let prep = match imp.prepare().await? {
743 PrepareResult::AlreadyPresent(c) => {
744 println!("No changes in {imgref:#} => {}", c.manifest_digest);
745 return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into())));
746 }
747 PrepareResult::Ready(p) => p,
748 };
749 check_bootc_label(&prep.config);
750 if let Some(warning) = prep.deprecated_warning() {
751 ostree_ext::cli::print_deprecated_warning(warning).await;
752 }
753 ostree_ext::cli::print_layer_status(&prep);
754 let layers_to_fetch = prep.layers_to_fetch().collect::<Result<Vec<_>>>()?;
755
756 const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
758 tracing::info!(
759 message_id = PULLING_NEW_IMAGE_ID,
760 bootc.image.reference = &imgref.image,
761 bootc.image.transport = "containers-storage",
762 bootc.original_transport = &imgref.transport,
763 bootc.status = "importing_from_storage",
764 "Importing image from bootc storage: {}",
765 ostree_imgref
766 );
767
768 let prepared_image = PreparedImportMeta {
769 imp,
770 n_layers_to_fetch: layers_to_fetch.len(),
771 layers_total: prep.all_layers().count(),
772 bytes_to_fetch: layers_to_fetch.iter().map(|(l, _)| l.layer.size()).sum(),
773 bytes_total: prep.all_layers().map(|l| l.layer.size()).sum(),
774 digest: prep.manifest_digest.clone(),
775 prep,
776 };
777
778 Ok(PreparedPullResult::Ready(Box::new(prepared_image)))
779}
780
781pub(crate) async fn pull_unified(
783 repo: &ostree::Repo,
784 imgref: &ImageReference,
785 target_imgref: Option<&OstreeImageReference>,
786 quiet: bool,
787 prog: ProgressWriter,
788 store: &Storage,
789 booted_deployment: Option<&ostree::Deployment>,
790) -> Result<Box<ImageState>> {
791 let progress = PullProgress::new(quiet, prog);
792 match prepare_for_pull_unified(repo, imgref, target_imgref, store, booted_deployment).await? {
793 PreparedPullResult::AlreadyPresent(existing) => {
794 const IMAGE_ALREADY_PRESENT_ID: &str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9";
796 tracing::debug!(
797 message_id = IMAGE_ALREADY_PRESENT_ID,
798 bootc.image.reference = &imgref.image,
799 bootc.image.transport = &imgref.transport,
800 bootc.status = "already_present",
801 "Image already present: {}",
802 imgref
803 );
804 Ok(existing)
805 }
806 PreparedPullResult::Ready(prepared_image_meta) => {
807 check_disk_space_unified(
808 store.get_ensure_composefs()?.as_ref(),
809 &prepared_image_meta,
810 imgref,
811 )?;
812 let cs_imgref = ImageReference {
814 transport: "containers-storage".to_string(),
815 image: imgref.image.clone(),
816 signature: imgref.signature.clone(),
817 };
818 pull_from_prepared(&cs_imgref, progress, *prepared_image_meta).await
819 }
820 }
821}
822
823#[context("Pulling")]
824pub(crate) async fn pull_from_prepared(
825 imgref: &ImageReference,
826 progress: PullProgress,
827 mut prepared_image: PreparedImportMeta,
828) -> Result<Box<ImageState>> {
829 let layer_progress = prepared_image.imp.request_progress();
830 let layer_byte_progress = prepared_image.imp.request_layer_progress();
831 let digest = prepared_image.digest.clone();
832 let digest_imp = prepared_image.digest.clone();
833
834 let printer_progress = progress.clone();
835 let printer = tokio::task::spawn(async move {
836 handle_layer_progress_print(LayerProgressConfig {
837 layers: layer_progress,
838 layer_bytes: layer_byte_progress,
839 digest: digest.as_ref().into(),
840 n_layers_to_fetch: prepared_image.n_layers_to_fetch,
841 layers_total: prepared_image.layers_total,
842 bytes_to_download: prepared_image.bytes_to_fetch,
843 bytes_total: prepared_image.bytes_total,
844 progress: printer_progress,
845 })
846 .await
847 });
848 let import = prepared_image.imp.import(prepared_image.prep).await;
849 printer.await?;
850 progress
852 .json
853 .send(Event::ProgressSteps {
854 task: "importing".into(),
855 description: "Importing Image".into(),
856 id: digest_imp.clone().as_ref().into(),
857 steps_cached: 0,
858 steps: 1,
859 steps_total: 1,
860 subtasks: [SubTaskStep {
861 subtask: "importing".into(),
862 description: "Importing Image".into(),
863 id: "importing".into(),
864 completed: true,
865 }]
866 .into(),
867 })
868 .await;
869 let import = import?;
870 let imgref_canonicalized = imgref.clone().canonicalize()?;
871 tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
872
873 let is_unified_path = imgref.transport == "containers-storage";
875 if !is_unified_path {
876 const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8";
877
878 tracing::info!(
879 message_id = IMPORT_COMPLETE_JOURNAL_ID,
880 bootc.image.reference = &imgref.image,
881 bootc.image.transport = &imgref.transport,
882 bootc.manifest_digest = import.manifest_digest.as_ref(),
883 bootc.ostree_commit = &import.merge_commit,
884 "Successfully imported image: {}",
885 imgref
886 );
887 }
888
889 if let Some(msg) =
890 ostree_container::store::image_filtered_content_warning(&import.filtered_files)
891 .context("Image content warning")?
892 {
893 tracing::info!("{}", msg);
894 }
895 Ok(Box::new((*import).into()))
896}
897
898fn is_retryable_pull_error(error: &anyhow::Error) -> bool {
899 error.chain().any(|source| {
905 matches!(
906 source.downcast_ref::<ostree_ext::containers_image_proxy::Error>(),
907 Some(ostree_ext::containers_image_proxy::Error::BlobError(
908 ostree_ext::containers_image_proxy::GetBlobError::Retryable(_)
909 ))
910 )
911 })
912}
913
914fn pull_retry_delay(retry: u32) -> Duration {
915 PULL_RETRY_INITIAL_DELAY.saturating_mul(2_u32.saturating_pow(retry))
916}
917
918pub(crate) async fn retry_pull_operation<F, Fut, T>(
919 progress: &PullProgress,
920 mut operation: F,
921) -> Result<T>
922where
923 F: FnMut() -> Fut,
924 Fut: std::future::Future<Output = Result<T>>,
925{
926 let mut retries = 0;
927 loop {
928 match operation().await {
929 Ok(value) => return Ok(value),
930 Err(error) if retries < PULL_MAX_RETRIES && is_retryable_pull_error(&error) => {
931 let retry_delay = pull_retry_delay(retries);
932 let attempt = retries + 1;
933 let description = format!(
934 "Container image pull failed; retrying in {} seconds ({attempt}/{PULL_MAX_RETRIES}): {error}",
935 retry_delay.as_secs()
936 );
937 progress
938 .send(Event::ProgressSteps {
939 task: "pulling".into(),
940 description: description.into(),
941 id: "pull-retry".into(),
942 steps_cached: 0,
943 steps: attempt.into(),
944 steps_total: PULL_MAX_RETRIES.into(),
945 subtasks: Vec::new(),
946 })
947 .await;
948 retries += 1;
949 tokio::time::sleep(retry_delay).await;
950 }
951 Err(error) => return Err(error),
952 }
953 }
954}
955
956async fn pull_once(
957 repo: &ostree::Repo,
958 imgref: &ImageReference,
959 target_imgref: Option<&OstreeImageReference>,
960 progress: PullProgress,
961 booted_deployment: Option<&ostree::Deployment>,
962) -> Result<Box<ImageState>> {
963 match prepare_for_pull(repo, imgref, target_imgref, booted_deployment).await? {
964 PreparedPullResult::AlreadyPresent(existing) => {
965 const IMAGE_ALREADY_PRESENT_ID: &str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9";
967 tracing::debug!(
968 message_id = IMAGE_ALREADY_PRESENT_ID,
969 bootc.image.reference = &imgref.image,
970 bootc.image.transport = &imgref.transport,
971 bootc.status = "already_present",
972 "Image already present: {}",
973 imgref
974 );
975 Ok(existing)
976 }
977 PreparedPullResult::Ready(prepared_image_meta) => {
978 check_disk_space_ostree(repo, &prepared_image_meta, imgref)?;
980 const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
982 tracing::info!(
983 message_id = PULLING_NEW_IMAGE_ID,
984 bootc.image.reference = &imgref.image,
985 bootc.image.transport = &imgref.transport,
986 bootc.status = "pulling_new",
987 "Pulling new image: {}",
988 imgref
989 );
990 Ok(pull_from_prepared(imgref, progress, *prepared_image_meta).await?)
991 }
992 }
993}
994
995pub(crate) async fn pull(
997 repo: &ostree::Repo,
998 imgref: &ImageReference,
999 target_imgref: Option<&OstreeImageReference>,
1000 quiet: bool,
1001 prog: ProgressWriter,
1002 booted_deployment: Option<&ostree::Deployment>,
1003) -> Result<Box<ImageState>> {
1004 let progress = PullProgress::new(quiet, prog);
1005 let operation = || {
1006 pull_once(
1007 repo,
1008 imgref,
1009 target_imgref,
1010 progress.clone(),
1011 booted_deployment,
1012 )
1013 };
1014 if imgref.transport != "registry" {
1015 return operation().await;
1016 }
1017 retry_pull_operation(&progress, operation).await
1018}
1019
1020pub(crate) async fn wipe_ostree(sysroot: Sysroot) -> Result<()> {
1021 tokio::task::spawn_blocking(move || {
1022 sysroot
1023 .write_deployments(&[], gio::Cancellable::NONE)
1024 .context("removing deployments")
1025 })
1026 .await??;
1027
1028 Ok(())
1029}
1030
1031pub(crate) async fn cleanup(sysroot: &Storage) -> Result<()> {
1032 const CLEANUP_JOURNAL_ID: &str = "2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6";
1034
1035 tracing::info!(
1036 message_id = CLEANUP_JOURNAL_ID,
1037 "Starting cleanup of old images and deployments"
1038 );
1039
1040 let bound_prune = prune_container_store(sysroot);
1041
1042 let ostree = sysroot.get_ostree_cloned()?;
1044 let repo = ostree.repo();
1045 let repo_prune =
1046 ostree_ext::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
1047 let locked_sysroot = &SysrootLock::from_assumed_locked(&ostree);
1048 let cancellable = Some(cancellable);
1049 let repo = &repo;
1050 let txn = repo.auto_transaction(cancellable)?;
1051 let repo = txn.repo();
1052
1053 for ref_entry in repo
1055 .list_refs_ext(
1056 Some(BASE_IMAGE_PREFIX),
1057 ostree::RepoListRefsExtFlags::NONE,
1058 cancellable,
1059 )
1060 .context("Listing refs")?
1061 .keys()
1062 {
1063 repo.transaction_set_refspec(ref_entry, None);
1064 }
1065
1066 for (i, deployment) in ostree.deployments().into_iter().enumerate() {
1069 let commit = deployment.csum();
1070 if let Some(base) = get_base_commit(repo, &commit)? {
1071 repo.transaction_set_refspec(&format!("{BASE_IMAGE_PREFIX}/{i}"), Some(&base));
1072 }
1073 }
1074
1075 let pruned =
1076 ostree_container::deploy::prune(locked_sysroot).context("Pruning images")?;
1077 if !pruned.is_empty() {
1078 let size = glib::format_size(pruned.objsize);
1079 println!(
1080 "Pruned images: {} (layers: {}, objsize: {})",
1081 pruned.n_images, pruned.n_layers, size
1082 );
1083 } else {
1084 tracing::debug!("Nothing to prune");
1085 }
1086
1087 Ok(())
1088 });
1089
1090 tokio::try_join!(repo_prune, bound_prune)?;
1092 Ok(())
1093}
1094
1095#[context("Finding base commit")]
1097pub(crate) fn get_base_commit(repo: &ostree::Repo, commit: &str) -> Result<Option<String>> {
1098 let commitv = repo.load_commit(commit)?.0;
1099 let commitmeta = commitv.child_value(0);
1100 let commitmeta = &glib::VariantDict::new(Some(&commitmeta));
1101 let r = commitmeta.lookup::<String>(BOOTC_DERIVED_KEY)?;
1102 Ok(r)
1103}
1104
1105#[context("Writing deployment")]
1106async fn deploy(
1107 sysroot: &Storage,
1108 from: MergeState,
1109 image: &ImageState,
1110 origin: &glib::KeyFile,
1111 lock_finalization: bool,
1112) -> Result<Deployment> {
1113 let (stateroot, override_kargs) = match &from {
1118 MergeState::MergeDeployment(deployment) => {
1119 let kargs = crate::bootc_kargs::get_kargs(sysroot, &deployment, image)?;
1120 (deployment.stateroot().into(), Some(kargs))
1121 }
1122 MergeState::Reset { stateroot, kargs } => (stateroot.clone(), Some(kargs.clone())),
1123 };
1124 let ostree = sysroot.get_ostree_cloned()?;
1126 let merge_deployment = from.as_merge_deployment();
1128 let merge_deployment = merge_deployment.map(|d| d.index() as usize);
1129 let ostree_commit = image.ostree_commit.to_string();
1130 let origin_data = origin.to_data();
1132 let r = async_task_with_spinner(
1133 "Deploying",
1134 spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1135 let ostree = ostree;
1136 let stateroot = Some(stateroot);
1137 let mut opts = ostree::SysrootDeployTreeOpts::default();
1138
1139 opts.locked = lock_finalization;
1141
1142 let override_kargs_refs = override_kargs
1145 .as_ref()
1146 .map(|kargs| kargs.iter_str().collect::<Vec<_>>());
1147 if let Some(kargs) = override_kargs_refs.as_ref() {
1148 opts.override_kernel_argv = Some(kargs);
1149 }
1150
1151 let deployments = ostree.deployments();
1152 let merge_deployment = merge_deployment.map(|m| &deployments[m]);
1153 let origin = glib::KeyFile::new();
1154 origin.load_from_data(&origin_data, glib::KeyFileFlags::NONE)?;
1155 let d = ostree.stage_tree_with_options(
1156 stateroot.as_deref(),
1157 &ostree_commit,
1158 Some(&origin),
1159 merge_deployment,
1160 &opts,
1161 Some(cancellable),
1162 )?;
1163 Ok(d.index())
1164 }),
1165 )
1166 .await?;
1167 let ostree = sysroot.get_ostree()?;
1169 let staged = ostree.staged_deployment().unwrap();
1170 assert_eq!(staged.index(), r);
1171 Ok(staged)
1172}
1173
1174#[context("Generating origin")]
1175fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
1176 let origin = glib::KeyFile::new();
1177 let imgref = OstreeImageReference::from(imgref.clone());
1178 origin.set_string(
1179 "origin",
1180 ostree_container::deploy::ORIGIN_CONTAINER,
1181 imgref.to_string().as_str(),
1182 );
1183 Ok(origin)
1184}
1185
1186#[derive(Debug)]
1188pub(crate) enum MergeState {
1189 MergeDeployment(Deployment),
1191 Reset {
1194 stateroot: String,
1195 kargs: CmdlineOwned,
1196 },
1197}
1198impl MergeState {
1199 pub(crate) fn from_stateroot(sysroot: &Storage, stateroot: &str) -> Result<Self> {
1201 let ostree = sysroot.get_ostree()?;
1202 let merge_deployment = ostree.merge_deployment(Some(stateroot)).ok_or_else(|| {
1203 anyhow::anyhow!("No merge deployment found for stateroot {stateroot}")
1204 })?;
1205 Ok(Self::MergeDeployment(merge_deployment))
1206 }
1207
1208 pub(crate) fn as_merge_deployment(&self) -> Option<&Deployment> {
1210 match self {
1211 Self::MergeDeployment(d) => Some(d),
1212 Self::Reset { .. } => None,
1213 }
1214 }
1215}
1216
1217#[context("Pulling bound images for ostree commit {commit}")]
1219async fn pull_bound_images_for_commit(sysroot: &Storage, commit: &str) -> Result<()> {
1220 let repo = sysroot.get_ostree()?.repo();
1221 let repo_dir = Dir::reopen_dir(&repo.dfd_borrow())?;
1222 let repo_tmp = repo_dir
1223 .open_dir("tmp")
1224 .context("Opening ostree repo tmp/")?;
1225 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1226 let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
1227 ostree::RepoCheckoutMode::None
1228 } else {
1229 ostree::RepoCheckoutMode::User
1230 };
1231 let checkout_opts = ostree::RepoCheckoutAtOptions {
1232 mode: checkout_mode,
1233 ..Default::default()
1234 };
1235 let root_name = "root";
1236 repo.checkout_at(
1237 Some(&checkout_opts),
1238 td.as_raw_fd(),
1239 root_name,
1240 commit,
1241 gio::Cancellable::NONE,
1242 )
1243 .context("Checking out imported commit")?;
1244 let root = td.open_dir(root_name)?;
1245 let bound_images = crate::boundimage::query_bound_images(&root)?;
1246 crate::boundimage::pull_images(sysroot, bound_images).await
1247}
1248
1249#[context("Staging")]
1251pub(crate) async fn stage(
1252 sysroot: &Storage,
1253 from: MergeState,
1254 image: &ImageState,
1255 spec: &RequiredHostSpec<'_>,
1256 prog: ProgressWriter,
1257 lock_finalization: bool,
1258) -> Result<()> {
1259 const STAGE_JOURNAL_ID: &str = "8f7a2b1c3d4e5f6a7b8c9d0e1f2a3b4c";
1261
1262 tracing::info!(
1263 message_id = STAGE_JOURNAL_ID,
1264 bootc.image.reference = &spec.image.image,
1265 bootc.image.transport = &spec.image.transport,
1266 bootc.manifest_digest = image.manifest_digest.as_ref(),
1267 "Staging image for deployment: {} (digest: {})",
1268 spec.image,
1269 image.manifest_digest
1270 );
1271
1272 let mut subtask = SubTaskStep {
1273 subtask: "merging".into(),
1274 description: "Merging Image".into(),
1275 id: "fetching".into(),
1276 completed: false,
1277 };
1278 let mut subtasks = vec![];
1279 prog.send(Event::ProgressSteps {
1280 task: "staging".into(),
1281 description: "Deploying Image".into(),
1282 id: image.manifest_digest.clone().as_ref().into(),
1283 steps_cached: 0,
1284 steps: 0,
1285 steps_total: 3,
1286 subtasks: subtasks
1287 .clone()
1288 .into_iter()
1289 .chain([subtask.clone()])
1290 .collect(),
1291 })
1292 .await;
1293
1294 subtask.completed = true;
1295 subtasks.push(subtask.clone());
1296 subtask.subtask = "bound_images".into();
1297 subtask.id = "bound_images".into();
1298 subtask.description = "Pulling Bound Images".into();
1299 subtask.completed = false;
1300 prog.send(Event::ProgressSteps {
1301 task: "staging".into(),
1302 description: "Deploying Image".into(),
1303 id: image.manifest_digest.clone().as_ref().into(),
1304 steps_cached: 0,
1305 steps: 1,
1306 steps_total: 3,
1307 subtasks: subtasks
1308 .clone()
1309 .into_iter()
1310 .chain([subtask.clone()])
1311 .collect(),
1312 })
1313 .await;
1314 pull_bound_images_for_commit(sysroot, &image.ostree_commit).await?;
1315
1316 subtask.completed = true;
1317 subtasks.push(subtask.clone());
1318 subtask.subtask = "deploying".into();
1319 subtask.id = "deploying".into();
1320 subtask.description = "Deploying Image".into();
1321 subtask.completed = false;
1322 prog.send(Event::ProgressSteps {
1323 task: "staging".into(),
1324 description: "Deploying Image".into(),
1325 id: image.manifest_digest.clone().as_ref().into(),
1326 steps_cached: 0,
1327 steps: 1,
1328 steps_total: 3,
1329 subtasks: subtasks
1330 .clone()
1331 .into_iter()
1332 .chain([subtask.clone()])
1333 .collect(),
1334 })
1335 .await;
1336 let origin = origin_from_imageref(spec.image)?;
1337 crate::deploy::deploy(sysroot, from, image, &origin, lock_finalization).await?;
1338
1339 subtask.completed = true;
1340 subtasks.push(subtask.clone());
1341 subtask.subtask = "cleanup".into();
1342 subtask.id = "cleanup".into();
1343 subtask.description = "Removing old images".into();
1344 subtask.completed = false;
1345 prog.send(Event::ProgressSteps {
1346 task: "staging".into(),
1347 description: "Deploying Image".into(),
1348 id: image.manifest_digest.clone().as_ref().into(),
1349 steps_cached: 0,
1350 steps: 2,
1351 steps_total: 3,
1352 subtasks: subtasks
1353 .clone()
1354 .into_iter()
1355 .chain([subtask.clone()])
1356 .collect(),
1357 })
1358 .await;
1359 crate::deploy::cleanup(sysroot).await?;
1360
1361 if !lock_finalization {
1362 println!("Queued for next boot: {:#}", spec.image);
1363 } else {
1364 println!("Staged but not queued for next boot: {:#}", spec.image);
1365 }
1366
1367 if let Some(version) = image.version.as_deref() {
1368 println!(" Version: {version}");
1369 }
1370 println!(" Digest: {}", image.manifest_digest);
1371
1372 subtask.completed = true;
1373 subtasks.push(subtask.clone());
1374 prog.send(Event::ProgressSteps {
1375 task: "staging".into(),
1376 description: "Deploying Image".into(),
1377 id: image.manifest_digest.clone().as_ref().into(),
1378 steps_cached: 0,
1379 steps: 3,
1380 steps_total: 3,
1381 subtasks: subtasks
1382 .clone()
1383 .into_iter()
1384 .chain([subtask.clone()])
1385 .collect(),
1386 })
1387 .await;
1388
1389 write_reboot_required(&image.manifest_digest.as_ref())?;
1392
1393 Ok(())
1394}
1395
1396fn write_reboot_required(image: &str) -> Result<()> {
1398 let reboot_message = format!("bootc: Reboot required for image: {}", image);
1399 let run_dir = Dir::open_ambient_dir("/run", cap_std::ambient_authority())?;
1400 run_dir
1401 .atomic_write("reboot-required", reboot_message.as_bytes())
1402 .context("Creating /run/reboot-required")?;
1403
1404 Ok(())
1405}
1406
1407pub(crate) const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468";
1408
1409pub(crate) async fn rollback(sysroot: &Storage) -> Result<()> {
1411 let ostree = sysroot.get_ostree()?;
1412 let (booted_ostree, deployments, host) = crate::status::get_status_require_booted(ostree)?;
1413
1414 let new_spec = {
1415 let mut new_spec = host.spec.clone();
1416 new_spec.boot_order = new_spec.boot_order.swap();
1417 new_spec
1418 };
1419
1420 let repo = &booted_ostree.repo();
1421
1422 host.spec.verify_transition(&new_spec)?;
1424
1425 let reverting = new_spec.boot_order == BootOrder::Default;
1426 if reverting {
1427 println!("notice: Reverting queued rollback state");
1428 }
1429 let rollback_status = host
1430 .status
1431 .rollback
1432 .ok_or_else(|| anyhow!("No rollback available"))?;
1433 let rollback_image = rollback_status
1434 .query_image(repo)?
1435 .ok_or_else(|| anyhow!("Rollback is not container image based"))?;
1436
1437 let current_image = host
1439 .status
1440 .booted
1441 .as_ref()
1442 .and_then(|b| b.query_image(repo).ok()?);
1443
1444 tracing::info!(
1445 message_id = ROLLBACK_JOURNAL_ID,
1446 bootc.manifest_digest = rollback_image.manifest_digest.as_ref(),
1447 bootc.ostree_commit = &rollback_image.merge_commit,
1448 bootc.rollback_type = if reverting { "revert" } else { "rollback" },
1449 bootc.current_manifest_digest = current_image
1450 .as_ref()
1451 .map(|i| i.manifest_digest.as_ref())
1452 .unwrap_or("none"),
1453 "Rolling back to image: {}",
1454 rollback_image.manifest_digest
1455 );
1456 let rollback_deployment = deployments.rollback.expect("rollback deployment");
1458 let new_deployments = if reverting {
1459 [booted_ostree.deployment, rollback_deployment]
1460 } else {
1461 [rollback_deployment, booted_ostree.deployment]
1462 };
1463 let new_deployments = new_deployments
1464 .into_iter()
1465 .chain(deployments.other)
1466 .collect::<Vec<_>>();
1467 tracing::debug!("Writing new deployments: {new_deployments:?}");
1468 booted_ostree
1469 .sysroot
1470 .write_deployments(&new_deployments, gio::Cancellable::NONE)?;
1471 if reverting {
1472 println!("Next boot: current deployment");
1473 } else {
1474 println!("Next boot: rollback deployment");
1475 }
1476
1477 write_reboot_required(rollback_image.manifest_digest.as_ref())?;
1478
1479 sysroot.update_mtime()?;
1480
1481 Ok(())
1482}
1483
1484fn find_newest_deployment_name(deploysdir: &Dir) -> Result<String> {
1485 let mut dirs = Vec::new();
1486 for ent in deploysdir.entries()? {
1487 let ent = ent?;
1488 if !ent.file_type()?.is_dir() {
1489 continue;
1490 }
1491 let name = ent.file_name();
1492 let Some(name) = name.to_str() else {
1493 continue;
1494 };
1495 dirs.push((name.to_owned(), ent.metadata()?.mtime()));
1496 }
1497 dirs.sort_unstable_by(|a, b| a.1.cmp(&b.1));
1498 if let Some((name, _ts)) = dirs.pop() {
1499 Ok(name)
1500 } else {
1501 anyhow::bail!("No deployment directory found")
1502 }
1503}
1504
1505pub(crate) fn switch_origin_inplace(root: &Dir, imgref: &ImageReference) -> Result<String> {
1507 const SWITCH_INPLACE_JOURNAL_ID: &str = "3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7";
1509
1510 tracing::info!(
1511 message_id = SWITCH_INPLACE_JOURNAL_ID,
1512 bootc.image.reference = &imgref.image,
1513 bootc.image.transport = &imgref.transport,
1514 bootc.switch_type = "in_place",
1515 "Performing in-place switch to image: {}",
1516 imgref
1517 );
1518
1519 let origin = origin_from_imageref(imgref)?;
1521 let serialized_origin = origin.to_data();
1522
1523 let mut ostree_deploys = root.open_dir("sysroot/ostree/deploy")?.entries()?;
1532 let deploydir = loop {
1533 if let Some(ent) = ostree_deploys.next() {
1534 let ent = ent?;
1535 if !ent.file_type()?.is_dir() {
1536 continue;
1537 }
1538 tracing::debug!("Checking {:?}", ent.file_name());
1539 let child_dir = ent
1540 .open_dir()
1541 .with_context(|| format!("Opening dir {:?}", ent.file_name()))?;
1542 if let Some(d) = child_dir.open_dir_optional("deploy")? {
1543 break d;
1544 }
1545 } else {
1546 anyhow::bail!("Failed to find a deployment");
1547 }
1548 };
1549 let newest_deployment = find_newest_deployment_name(&deploydir)?;
1550 let origin_path = format!("{newest_deployment}.origin");
1551 if !deploydir.try_exists(&origin_path)? {
1552 tracing::warn!("No extant origin for {newest_deployment}");
1553 }
1554 deploydir
1555 .atomic_write(&origin_path, serialized_origin.as_bytes())
1556 .context("Writing origin")?;
1557 Ok(newest_deployment)
1558}
1559
1560#[context("Updating /etc/fstab for anaconda+composefs")]
1563pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> {
1564 let fstab_path = "etc/fstab";
1565 let fd = root
1567 .open(fstab_path)
1568 .with_context(|| format!("Opening {fstab_path}"))
1569 .map(std::io::BufReader::new)?;
1570
1571 fn edit_fstab_line(line: &str, mut w: impl Write) -> Result<bool> {
1575 if line.starts_with('#') {
1576 return Ok(false);
1577 }
1578 let parts = line.split_ascii_whitespace().collect::<Vec<_>>();
1579
1580 let path_idx = 1;
1581 let options_idx = 3;
1582 let (&path, &options) = match (parts.get(path_idx), parts.get(options_idx)) {
1583 (None, _) => {
1584 tracing::debug!("No path in entry: {line}");
1585 return Ok(false);
1586 }
1587 (_, None) => {
1588 tracing::debug!("No options in entry: {line}");
1589 return Ok(false);
1590 }
1591 (Some(p), Some(o)) => (p, o),
1592 };
1593 if path != "/" {
1595 return Ok(false);
1596 }
1597 if options.split(',').any(|s| s == "ro") {
1599 return Ok(false);
1600 }
1601
1602 writeln!(w, "# {}", crate::generator::BOOTC_EDITED_STAMP)?;
1603
1604 assert!(!options.is_empty()); let options = format!("{options},ro");
1608 for (i, part) in parts.into_iter().enumerate() {
1609 if i > 0 {
1611 write!(w, " ")?;
1612 }
1613 if i == options_idx {
1614 write!(w, "{options}")?;
1615 } else {
1616 write!(w, "{part}")?
1617 }
1618 }
1619 writeln!(w)?;
1621 Ok(true)
1622 }
1623
1624 root.atomic_replace_with(fstab_path, move |mut w| -> Result<()> {
1626 for line in fd.lines() {
1627 let line = line?;
1628 if !edit_fstab_line(&line, &mut w)? {
1629 writeln!(w, "{line}")?;
1630 }
1631 }
1632 Ok(())
1633 })
1634 .context("Replacing /etc/fstab")?;
1635
1636 println!("Updated /etc/fstab to add `ro` for `/`");
1637 Ok(())
1638}
1639
1640#[cfg(test)]
1641mod tests {
1642 use super::*;
1643 use tokio::io::{AsyncBufReadExt, BufReader};
1644
1645 fn get_blob_failure(error: ostree_ext::containers_image_proxy::GetBlobError) -> anyhow::Error {
1646 let error = ostree_ext::containers_image_proxy::Error::BlobError(error);
1647 anyhow::Error::from(error).context("Unencapsulating base")
1648 }
1649
1650 fn retryable_blob_failure(message: &str) -> anyhow::Error {
1651 get_blob_failure(ostree_ext::containers_image_proxy::GetBlobError::Retryable(
1652 message.into(),
1653 ))
1654 }
1655
1656 fn permanent_blob_failure(message: &str) -> anyhow::Error {
1657 get_blob_failure(ostree_ext::containers_image_proxy::GetBlobError::Other(
1658 message.into(),
1659 ))
1660 }
1661
1662 fn open_image_failure(message: &str) -> anyhow::Error {
1663 let error = ostree_ext::containers_image_proxy::Error::RequestInitiationFailure {
1664 method: "OpenImage".into(),
1665 error: message.into(),
1666 };
1667 anyhow::Error::from(error).context("Creating importer")
1668 }
1669
1670 #[test]
1671 fn test_retryable_pull_error_classification() {
1672 let cases = [
1673 (retryable_blob_failure("502 Bad Gateway"), true),
1674 (permanent_blob_failure("blob unknown"), false),
1675 (open_image_failure("image unknown"), false),
1676 ];
1677
1678 for (error, expected) in cases {
1679 assert_eq!(is_retryable_pull_error(&error), expected, "{error:#}");
1680 }
1681 }
1682
1683 #[tokio::test(start_paused = true)]
1684 async fn test_retry_pull_operation_succeeds() -> Result<()> {
1685 let attempts = std::cell::Cell::new(0);
1686 let start = tokio::time::Instant::now();
1687 let progress = PullProgress::new(true, ProgressWriter::default());
1688 let value = retry_pull_operation(&progress, || {
1689 let attempt = attempts.get() + 1;
1690 attempts.set(attempt);
1691 async move {
1692 if attempt == 1 {
1693 Err(retryable_blob_failure("502 Bad Gateway"))
1694 } else {
1695 Ok(42)
1696 }
1697 }
1698 })
1699 .await?;
1700
1701 assert_eq!(value, 42);
1702 assert_eq!(attempts.get(), 2);
1703 assert_eq!(start.elapsed(), pull_retry_delay(0));
1704 Ok(())
1705 }
1706
1707 #[tokio::test(start_paused = true)]
1708 async fn test_retry_pull_operation_reports_progress() -> Result<()> {
1709 let attempts = std::cell::Cell::new(0);
1710 let (send, recv) = tokio::net::unix::pipe::pipe()?;
1711 let progress = PullProgress::new(true, ProgressWriter::from(send));
1712
1713 retry_pull_operation(&progress, || {
1714 let attempt = attempts.get() + 1;
1715 attempts.set(attempt);
1716 async move {
1717 if attempt == 1 {
1718 Err(retryable_blob_failure("502 Bad Gateway"))
1719 } else {
1720 Ok(())
1721 }
1722 }
1723 })
1724 .await?;
1725
1726 let mut lines = BufReader::new(recv).lines();
1727 let start_line = lines.next_line().await?.unwrap();
1728 let start: Event = serde_json::from_str(&start_line)?;
1729 assert!(matches!(start, Event::Start { version } if version == "0.1.0"));
1730
1731 let retry_line = lines.next_line().await?.unwrap();
1732 let retry: Event = serde_json::from_str(&retry_line)?;
1733 assert_eq!(
1734 retry,
1735 Event::ProgressSteps {
1736 task: "pulling".into(),
1737 description:
1738 "Container image pull failed; retrying in 2 seconds (1/3): Unencapsulating base"
1739 .into(),
1740 id: "pull-retry".into(),
1741 steps_cached: 0,
1742 steps: 1,
1743 steps_total: 3,
1744 subtasks: Vec::new(),
1745 }
1746 );
1747 drop(progress);
1748 assert!(lines.next_line().await?.is_none());
1749 Ok(())
1750 }
1751
1752 #[tokio::test(start_paused = true)]
1753 async fn test_retry_pull_operation_stops_after_max_attempts() {
1754 let attempts = std::cell::Cell::new(0);
1755 let start = tokio::time::Instant::now();
1756 let progress = PullProgress::new(true, ProgressWriter::default());
1757 let error = retry_pull_operation(&progress, || {
1758 attempts.set(attempts.get() + 1);
1759 async { Err::<(), _>(retryable_blob_failure("registry unavailable")) }
1760 })
1761 .await
1762 .unwrap_err();
1763
1764 let expected_delay = (0..PULL_MAX_RETRIES)
1765 .map(pull_retry_delay)
1766 .fold(Duration::ZERO, Duration::saturating_add);
1767 assert_eq!(attempts.get(), PULL_MAX_RETRIES + 1);
1768 assert_eq!(start.elapsed(), expected_delay);
1769 assert_eq!(
1770 error.root_cause().to_string(),
1771 "retryable error: registry unavailable"
1772 );
1773 }
1774
1775 #[tokio::test(start_paused = true)]
1776 async fn test_retry_pull_operation_does_not_retry_permanent_blob_error() {
1777 let attempts = std::cell::Cell::new(0);
1778 let progress = PullProgress::new(true, ProgressWriter::default());
1779 let error = retry_pull_operation(&progress, || {
1780 attempts.set(attempts.get() + 1);
1781 async { Err::<(), _>(permanent_blob_failure("blob unknown")) }
1782 })
1783 .await
1784 .unwrap_err();
1785
1786 assert_eq!(attempts.get(), 1);
1787 assert_eq!(error.root_cause().to_string(), "other error: blob unknown");
1788 }
1789
1790 #[tokio::test(start_paused = true)]
1791 async fn test_retry_pull_operation_does_not_retry_opaque_prepare_error() {
1792 let attempts = std::cell::Cell::new(0);
1793 let progress = PullProgress::new(true, ProgressWriter::default());
1794 let error = retry_pull_operation(&progress, || {
1795 attempts.set(attempts.get() + 1);
1796 async { Err::<(), _>(open_image_failure("image unknown")) }
1797 })
1798 .await
1799 .unwrap_err();
1800
1801 assert_eq!(attempts.get(), 1);
1802 assert_eq!(
1803 error.root_cause().to_string(),
1804 "failed to invoke method OpenImage: image unknown"
1805 );
1806 }
1807
1808 #[test]
1809 fn test_new_proxy_config_user_agent() {
1810 let config = new_proxy_config();
1811 let prefix = config
1812 .user_agent_prefix
1813 .expect("user_agent_prefix should be set");
1814 assert!(
1815 prefix.starts_with("bootc/"),
1816 "User agent should start with bootc/"
1817 );
1818 assert!(
1820 prefix.len() > "bootc/".len(),
1821 "Version should be present after bootc/"
1822 );
1823 }
1824
1825 #[test]
1826 fn test_switch_inplace() -> Result<()> {
1827 use cap_std::fs::DirBuilderExt;
1828
1829 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1830 let mut builder = cap_std::fs::DirBuilder::new();
1831 let builder = builder.recursive(true).mode(0o755);
1832 let deploydir = "sysroot/ostree/deploy/default/deploy";
1833 let target_deployment =
1834 "af36eb0086bb55ac601600478c6168f834288013d60f8870b7851f44bf86c3c5.0";
1835 td.ensure_dir_with(
1836 format!("sysroot/ostree/deploy/default/deploy/{target_deployment}"),
1837 builder,
1838 )?;
1839 let deploydir = &td.open_dir(deploydir)?;
1840 let orig_imgref = ImageReference {
1841 image: "quay.io/exampleos/original:sometag".into(),
1842 transport: "registry".into(),
1843 signature: None,
1844 };
1845 {
1846 let origin = origin_from_imageref(&orig_imgref)?;
1847 deploydir.atomic_write(
1848 format!("{target_deployment}.origin"),
1849 origin.to_data().as_bytes(),
1850 )?;
1851 }
1852
1853 let target_imgref = ImageReference {
1854 image: "quay.io/someother/otherimage:latest".into(),
1855 transport: "registry".into(),
1856 signature: None,
1857 };
1858
1859 let replaced = switch_origin_inplace(&td, &target_imgref).unwrap();
1860 assert_eq!(replaced, target_deployment);
1861 Ok(())
1862 }
1863
1864 #[test]
1865 fn test_fixup_etc_fstab_default() -> Result<()> {
1866 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1867 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n";
1868 tempdir.create_dir_all("etc")?;
1869 tempdir.atomic_write("etc/fstab", default)?;
1870 fixup_etc_fstab(&tempdir).unwrap();
1871 assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
1872 Ok(())
1873 }
1874
1875 #[test]
1876 fn test_fixup_etc_fstab_multi() -> Result<()> {
1877 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1878 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1879UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1880 tempdir.create_dir_all("etc")?;
1881 tempdir.atomic_write("etc/fstab", default)?;
1882 fixup_etc_fstab(&tempdir).unwrap();
1883 assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
1884 Ok(())
1885 }
1886
1887 #[test]
1888 fn test_fixup_etc_fstab_ro() -> Result<()> {
1889 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1890 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1891UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs ro 0 0\n\
1892UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1893 tempdir.create_dir_all("etc")?;
1894 tempdir.atomic_write("etc/fstab", default)?;
1895 fixup_etc_fstab(&tempdir).unwrap();
1896 assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
1897 Ok(())
1898 }
1899
1900 #[test]
1901 fn test_fixup_etc_fstab_rw() -> Result<()> {
1902 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1903 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1905UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults 0 0\n\
1906UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1907 let modified = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1908# Updated by bootc-fstab-edit.service\n\
1909UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults,ro 0 0\n\
1910UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1911 tempdir.create_dir_all("etc")?;
1912 tempdir.atomic_write("etc/fstab", default)?;
1913 fixup_etc_fstab(&tempdir).unwrap();
1914 assert_eq!(tempdir.read_to_string("etc/fstab")?, modified);
1915 Ok(())
1916 }
1917 #[test]
1918 fn test_check_disk_space_inner() -> Result<()> {
1919 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1920 let imgref = ImageReference {
1921 image: "quay.io/exampleos/exampleos:latest".into(),
1922 transport: "registry".into(),
1923 signature: None,
1924 };
1925
1926 check_disk_space_inner(&*td, 0, 0, &imgref)?;
1928
1929 assert!(check_disk_space_inner(&*td, u64::MAX, 0, &imgref).is_err());
1931
1932 assert!(check_disk_space_inner(&*td, 1, u64::MAX, &imgref).is_err());
1934
1935 Ok(())
1936 }
1937}