1use std::collections::HashSet;
48use std::io::{BufRead, Write};
49use std::os::fd::AsFd;
50use std::process::Command;
51
52use anyhow::{Context, Result, anyhow};
53use bootc_kernel_cmdline::utf8::CmdlineOwned;
54use bootc_utils::skopeo_bin;
55use cap_std::fs::{Dir, MetadataExt};
56use cap_std_ext::cap_std;
57use cap_std_ext::dirext::CapStdExtDirExt;
58use fn_error_context::context;
59use ostree::{gio, glib};
60use ostree_container::OstreeImageReference;
61use ostree_ext::container as ostree_container;
62use ostree_ext::container::store::{ImageImporter, ImportProgress, PrepareResult, PreparedImport};
63use ostree_ext::oci_spec::image::{Descriptor, Digest};
64use ostree_ext::ostree::Deployment;
65use ostree_ext::ostree::{self, Sysroot};
66use ostree_ext::sysroot::SysrootLock;
67use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten;
68
69use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep};
70use crate::spec::ImageReference;
71use crate::spec::{BootOrder, HostSpec};
72use crate::status::labels_of_config;
73use crate::store::Storage;
74use crate::utils::async_task_with_spinner;
75
76const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc";
78
79pub(crate) fn new_proxy_config() -> ostree_ext::containers_image_proxy::ImageProxyConfig {
84 let mut c = ostree_ext::containers_image_proxy::ImageProxyConfig::default();
85 c.user_agent_prefix = Some(format!("bootc/{}", env!("CARGO_PKG_VERSION")));
86 c
87}
88
89const BOOTC_DERIVED_KEY: &str = "bootc.derived";
91
92pub(crate) struct RequiredHostSpec<'a> {
94 pub(crate) image: &'a ImageReference,
95}
96
97pub(crate) struct ImageState {
99 pub(crate) manifest_digest: Digest,
100 pub(crate) version: Option<String>,
101 pub(crate) ostree_commit: String,
102}
103
104impl<'a> RequiredHostSpec<'a> {
105 pub(crate) fn from_spec(spec: &'a HostSpec) -> Result<Self> {
108 let image = spec
109 .image
110 .as_ref()
111 .ok_or_else(|| anyhow::anyhow!("Missing image in specification"))?;
112 Ok(Self { image })
113 }
114}
115
116impl From<ostree_container::store::LayeredImageState> for ImageState {
117 fn from(value: ostree_container::store::LayeredImageState) -> Self {
118 let version = value.version().map(|v| v.to_owned());
119 let ostree_commit = value.get_commit().to_owned();
120 Self {
121 manifest_digest: value.manifest_digest,
122 version,
123 ostree_commit,
124 }
125 }
126}
127
128impl ImageState {
129 pub(crate) fn get_manifest(
131 &self,
132 repo: &ostree::Repo,
133 ) -> Result<Option<ostree_ext::oci_spec::image::ImageManifest>> {
134 ostree_container::store::query_image_commit(repo, &self.ostree_commit)
135 .map(|v| Some(v.manifest))
136 }
137}
138
139pub(crate) async fn new_importer(
141 repo: &ostree::Repo,
142 imgref: &ostree_container::OstreeImageReference,
143 booted_deployment: Option<&ostree::Deployment>,
144) -> Result<ostree_container::store::ImageImporter> {
145 let config = new_proxy_config();
146 let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?;
147 imp.require_bootable();
148 imp.disable_gc();
150 if let Some(deployment) = booted_deployment {
151 imp.set_sepolicy_commit(deployment.csum().to_string());
152 }
153 Ok(imp)
154}
155
156pub(crate) async fn new_importer_with_config(
158 repo: &ostree::Repo,
159 imgref: &ostree_container::OstreeImageReference,
160 config: ostree_ext::containers_image_proxy::ImageProxyConfig,
161 booted_deployment: Option<&ostree::Deployment>,
162) -> Result<ostree_container::store::ImageImporter> {
163 let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?;
164 imp.require_bootable();
165 imp.disable_gc();
167 if let Some(deployment) = booted_deployment {
168 imp.set_sepolicy_commit(deployment.csum().to_string());
169 }
170 Ok(imp)
171}
172
173pub(crate) fn check_bootc_label(config: &ostree_ext::oci_spec::image::ImageConfiguration) {
174 if let Some(label) =
175 labels_of_config(config).and_then(|labels| labels.get(crate::metadata::BOOTC_COMPAT_LABEL))
176 {
177 match label.as_str() {
178 crate::metadata::COMPAT_LABEL_V1 => {}
179 o => crate::journal::journal_print(
180 libsystemd::logging::Priority::Warning,
181 &format!(
182 "notice: Unknown {} value {}",
183 crate::metadata::BOOTC_COMPAT_LABEL,
184 o
185 ),
186 ),
187 }
188 } else {
189 crate::journal::journal_print(
190 libsystemd::logging::Priority::Warning,
191 &format!(
192 "notice: Image is missing label: {}",
193 crate::metadata::BOOTC_COMPAT_LABEL
194 ),
195 )
196 }
197}
198
199fn descriptor_of_progress(p: &ImportProgress) -> &Descriptor {
200 match p {
201 ImportProgress::OstreeChunkStarted(l) => l,
202 ImportProgress::OstreeChunkCompleted(l) => l,
203 ImportProgress::DerivedLayerStarted(l) => l,
204 ImportProgress::DerivedLayerCompleted(l) => l,
205 }
206}
207
208fn prefix_of_progress(p: &ImportProgress) -> &'static str {
209 match p {
210 ImportProgress::OstreeChunkStarted(_) | ImportProgress::OstreeChunkCompleted(_) => {
211 "ostree chunk"
212 }
213 ImportProgress::DerivedLayerStarted(_) | ImportProgress::DerivedLayerCompleted(_) => {
214 "layer"
215 }
216 }
217}
218
219struct LayerProgressConfig {
221 layers: tokio::sync::mpsc::Receiver<ostree_container::store::ImportProgress>,
222 layer_bytes: tokio::sync::watch::Receiver<Option<ostree_container::store::LayerProgress>>,
223 digest: Box<str>,
224 n_layers_to_fetch: usize,
225 layers_total: usize,
226 bytes_to_download: u64,
227 bytes_total: u64,
228 prog: ProgressWriter,
229 quiet: bool,
230}
231
232async fn handle_layer_progress_print(mut config: LayerProgressConfig) -> ProgressWriter {
234 let start = std::time::Instant::now();
235 let mut total_read = 0u64;
236 let bar = indicatif::MultiProgress::new();
237 if config.quiet {
238 bar.set_draw_target(indicatif::ProgressDrawTarget::hidden());
239 }
240 let layers_bar = bar.add(indicatif::ProgressBar::new(
241 config.n_layers_to_fetch.try_into().unwrap(),
242 ));
243 let byte_bar = bar.add(indicatif::ProgressBar::new(0));
244 layers_bar.set_style(
247 indicatif::ProgressStyle::default_bar()
248 .template("{prefix} {bar} {pos}/{len} {wide_msg}")
249 .unwrap(),
250 );
251 let taskname = "Fetching layers";
252 layers_bar.set_prefix(taskname);
253 layers_bar.set_message("");
254 byte_bar.set_prefix("Fetching");
255 byte_bar.set_style(
256 indicatif::ProgressStyle::default_bar()
257 .template(
258 " └ {prefix} {bar} {binary_bytes}/{binary_total_bytes} ({binary_bytes_per_sec}) {wide_msg}",
259 )
260 .unwrap()
261 );
262
263 let mut subtasks = vec![];
264 let mut subtask: SubTaskBytes = Default::default();
265 loop {
266 tokio::select! {
267 biased;
269 layer = config.layers.recv() => {
270 if let Some(l) = layer {
271 let layer = descriptor_of_progress(&l);
272 let layer_type = prefix_of_progress(&l);
273 let short_digest = &layer.digest().digest()[0..21];
274 let layer_size = layer.size();
275 if l.is_starting() {
276 byte_bar.reset_elapsed();
278 byte_bar.reset_eta();
279 byte_bar.set_length(layer_size);
280 byte_bar.set_message(format!("{layer_type} {short_digest}"));
281
282 subtask = SubTaskBytes {
283 subtask: layer_type.into(),
284 description: format!("{layer_type}: {short_digest}").clone().into(),
285 id: short_digest.to_string().clone().into(),
286 bytes_cached: 0,
287 bytes: 0,
288 bytes_total: layer_size,
289 };
290 } else {
291 let actual_size = byte_bar.length().unwrap_or(layer_size);
294 byte_bar.set_position(actual_size);
295 layers_bar.inc(1);
296 total_read = total_read.saturating_add(actual_size);
297 subtask.bytes_total = actual_size;
299 subtask.bytes = actual_size;
300 subtasks.push(subtask.clone());
301 config.prog.send(Event::ProgressBytes {
302 task: "pulling".into(),
303 description: format!("Pulling Image: {}", config.digest).into(),
304 id: (*config.digest).into(),
305 bytes_cached: config.bytes_total - config.bytes_to_download,
306 bytes: total_read,
307 bytes_total: config.bytes_to_download,
308 steps_cached: (config.layers_total - config.n_layers_to_fetch) as u64,
309 steps: layers_bar.position(),
310 steps_total: config.n_layers_to_fetch as u64,
311 subtasks: subtasks.clone(),
312 }).await;
313 }
314 } else {
315 break
317 };
318 },
319 r = config.layer_bytes.changed() => {
320 if r.is_err() {
321 break
323 }
324 let bytes = {
325 let bytes = config.layer_bytes.borrow_and_update();
326 bytes.as_ref().cloned()
327 };
328 if let Some(bytes) = bytes {
329 byte_bar.set_length(bytes.total);
333 byte_bar.set_position(bytes.fetched);
334 subtask.bytes_total = bytes.total;
335 subtask.bytes = byte_bar.position();
336 config.prog.send_lossy(Event::ProgressBytes {
337 task: "pulling".into(),
338 description: format!("Pulling Image: {}", config.digest).into(),
339 id: (*config.digest).into(),
340 bytes_cached: config.bytes_total - config.bytes_to_download,
341 bytes: total_read + byte_bar.position(),
342 bytes_total: config.bytes_to_download,
343 steps_cached: (config.layers_total - config.n_layers_to_fetch) as u64,
344 steps: layers_bar.position(),
345 steps_total: config.n_layers_to_fetch as u64,
346 subtasks: subtasks.clone().into_iter().chain([subtask.clone()]).collect(),
347 }).await;
348 }
349 }
350 }
351 }
352 byte_bar.finish_and_clear();
353 layers_bar.finish_and_clear();
354 if let Err(e) = bar.clear() {
355 tracing::warn!("clearing bar: {e}");
356 }
357 let end = std::time::Instant::now();
358 let elapsed = end.duration_since(start);
359 let persec = total_read as f64 / elapsed.as_secs_f64();
360 let persec = indicatif::HumanBytes(persec as u64);
361 if let Err(e) = bar.println(&format!(
362 "Fetched layers: {} in {} ({}/s)",
363 indicatif::HumanBytes(total_read),
364 indicatif::HumanDuration(elapsed),
365 persec,
366 )) {
367 tracing::warn!("writing to stdout: {e}");
368 }
369
370 config
374 .prog
375 .send(Event::ProgressSteps {
376 task: "importing".into(),
377 description: "Importing Image".into(),
378 id: (*config.digest).into(),
379 steps_cached: 0,
380 steps: 0,
381 steps_total: 1,
382 subtasks: [SubTaskStep {
383 subtask: "importing".into(),
384 description: "Importing Image".into(),
385 id: "importing".into(),
386 completed: false,
387 }]
388 .into(),
389 })
390 .await;
391
392 config.prog
394}
395
396pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> {
399 let ostree = sysroot.get_ostree()?;
400 let deployments = ostree.deployments();
401 let mut all_bound_images = Vec::new();
402 for deployment in deployments {
403 let bound = crate::boundimage::query_bound_images_for_deployment(ostree, &deployment)?;
404 all_bound_images.extend(bound.into_iter());
405 if let Some(host_image) = crate::status::boot_entry_from_deployment(ostree, &deployment)?
409 .image
410 .map(|i| i.image)
411 {
412 all_bound_images.push(crate::boundimage::BoundImage {
413 image: host_image.image.clone(),
414 auth_file: None,
415 });
416 }
417 }
418 let image_names = HashSet::from_iter(all_bound_images.iter().map(|img| img.image.as_str()));
420 let pruned = sysroot
421 .get_ensure_imgstore()?
422 .prune_except_roots(&image_names)
423 .await?;
424 tracing::debug!("Pruned images: {}", pruned.len());
425 Ok(())
426}
427
428fn check_disk_space_inner(
431 fd: impl AsFd,
432 bytes_to_fetch: u64,
433 min_free: u64,
434 imgref: &ImageReference,
435) -> Result<()> {
436 let stat = rustix::fs::fstatvfs(fd)?;
437 let bytes_avail = stat.f_bsize.checked_mul(stat.f_bavail).unwrap_or(u64::MAX);
438 let usable = bytes_avail.saturating_sub(min_free);
439 tracing::trace!("bytes_avail: {bytes_avail} min_free: {min_free} usable: {usable}");
440
441 if bytes_to_fetch > usable {
442 anyhow::bail!(
443 "Insufficient free space for {image} (available: {available} required: {required})",
444 available = ostree_ext::glib::format_size(usable),
445 required = ostree_ext::glib::format_size(bytes_to_fetch),
446 image = imgref.image,
447 );
448 }
449 Ok(())
450}
451
452pub(crate) fn check_disk_space_ostree(
455 repo: &ostree::Repo,
456 image_meta: &PreparedImportMeta,
457 imgref: &ImageReference,
458) -> Result<()> {
459 let min_free = repo.min_free_space_bytes().unwrap_or(0);
460 check_disk_space_inner(
461 repo.dfd_borrow(),
462 image_meta.bytes_to_fetch,
463 min_free,
464 imgref,
465 )
466}
467
468pub(crate) fn check_disk_space_unified(
471 cfs: &crate::store::ComposefsRepository,
472 image_meta: &PreparedImportMeta,
473 imgref: &ImageReference,
474) -> Result<()> {
475 check_disk_space_inner(cfs.objects_dir()?, image_meta.bytes_to_fetch, 0, imgref)
476}
477
478pub(crate) fn check_disk_space_composefs(
481 cfs: &crate::store::ComposefsRepository,
482 manifest: &ostree_ext::oci_spec::image::ImageManifest,
483 imgref: &ImageReference,
484) -> Result<()> {
485 let bytes_to_fetch: u64 = manifest
486 .layers()
487 .iter()
488 .map(|l: &ostree_ext::oci_spec::image::Descriptor| l.size())
489 .sum();
490 check_disk_space_inner(cfs.objects_dir()?, bytes_to_fetch, 0, imgref)
491}
492
493pub(crate) struct PreparedImportMeta {
494 pub imp: ImageImporter,
495 pub prep: Box<PreparedImport>,
496 pub digest: Digest,
497 pub n_layers_to_fetch: usize,
498 pub layers_total: usize,
499 pub bytes_to_fetch: u64,
500 pub bytes_total: u64,
501}
502
503pub(crate) enum PreparedPullResult {
504 Ready(Box<PreparedImportMeta>),
505 AlreadyPresent(Box<ImageState>),
506}
507
508pub(crate) async fn prepare_for_pull(
509 repo: &ostree::Repo,
510 imgref: &ImageReference,
511 target_imgref: Option<&OstreeImageReference>,
512 booted_deployment: Option<&ostree::Deployment>,
513) -> Result<PreparedPullResult> {
514 let imgref_canonicalized = imgref.clone().canonicalize()?;
515 tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
516 let ostree_imgref = &OstreeImageReference::from(imgref_canonicalized);
517 let mut imp = new_importer(repo, ostree_imgref, booted_deployment).await?;
518 if let Some(target) = target_imgref {
519 imp.set_target(target);
520 }
521 let prep = match imp.prepare().await? {
522 PrepareResult::AlreadyPresent(c) => {
523 println!("No changes in {imgref:#} => {}", c.manifest_digest);
524 return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into())));
525 }
526 PrepareResult::Ready(p) => p,
527 };
528 check_bootc_label(&prep.config);
529 if let Some(warning) = prep.deprecated_warning() {
530 ostree_ext::cli::print_deprecated_warning(warning).await;
531 }
532 ostree_ext::cli::print_layer_status(&prep);
533 let layers_to_fetch = prep.layers_to_fetch().collect::<Result<Vec<_>>>()?;
534
535 let prepared_image = PreparedImportMeta {
536 imp,
537 n_layers_to_fetch: layers_to_fetch.len(),
538 layers_total: prep.all_layers().count(),
539 bytes_to_fetch: layers_to_fetch.iter().map(|(l, _)| l.layer.size()).sum(),
540 bytes_total: prep.all_layers().map(|l| l.layer.size()).sum(),
541 digest: prep.manifest_digest.clone(),
542 prep,
543 };
544
545 Ok(PreparedPullResult::Ready(Box::new(prepared_image)))
546}
547
548pub(crate) async fn image_exists_in_unified_storage(
556 store: &Storage,
557 imgref: &ImageReference,
558) -> Result<bool> {
559 let imgstore = store.get_ensure_imgstore()?;
560 let image_ref_str = imgref.to_transport_image()?;
561 imgstore.exists(&image_ref_str).await
562}
563
564pub(crate) async fn prepare_for_pull_unified(
567 repo: &ostree::Repo,
568 imgref: &ImageReference,
569 target_imgref: Option<&OstreeImageReference>,
570 store: &Storage,
571 booted_deployment: Option<&ostree::Deployment>,
572) -> Result<PreparedPullResult> {
573 let imgstore = store.get_ensure_imgstore()?;
575
576 let image_ref_str = imgref.to_transport_image()?;
577
578 tracing::info!(
581 "Unified pull: pulling from transport '{}' to bootc storage",
582 &imgref.transport
583 );
584
585 imgstore
588 .pull_with_progress(&image_ref_str)
589 .await
590 .context("Pulling image into bootc containers-storage")?;
591
592 tracing::info!("Unified pull: now importing from containers-storage transport");
594 let containers_storage_imgref = ImageReference {
595 transport: "containers-storage".to_string(),
596 image: imgref.image.clone(),
597 signature: imgref.signature.clone(),
598 };
599 let ostree_imgref = OstreeImageReference::from(containers_storage_imgref);
600
601 let mut config = new_proxy_config();
603 let mut cmd = Command::new(skopeo_bin());
604 let storage_path = format!(
606 "{}/{}",
607 store.physical_root_path,
608 crate::podstorage::CStorage::subpath()
609 );
610 crate::podstorage::set_additional_image_store(&mut cmd, &storage_path);
611 config.skopeo_cmd = Some(cmd);
612
613 let mut imp = new_importer_with_config(repo, &ostree_imgref, config, booted_deployment).await?;
615 if let Some(target) = target_imgref {
616 imp.set_target(target);
617 }
618 let prep = match imp.prepare().await? {
619 PrepareResult::AlreadyPresent(c) => {
620 println!("No changes in {imgref:#} => {}", c.manifest_digest);
621 return Ok(PreparedPullResult::AlreadyPresent(Box::new((*c).into())));
622 }
623 PrepareResult::Ready(p) => p,
624 };
625 check_bootc_label(&prep.config);
626 if let Some(warning) = prep.deprecated_warning() {
627 ostree_ext::cli::print_deprecated_warning(warning).await;
628 }
629 ostree_ext::cli::print_layer_status(&prep);
630 let layers_to_fetch = prep.layers_to_fetch().collect::<Result<Vec<_>>>()?;
631
632 const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
634 tracing::info!(
635 message_id = PULLING_NEW_IMAGE_ID,
636 bootc.image.reference = &imgref.image,
637 bootc.image.transport = "containers-storage",
638 bootc.original_transport = &imgref.transport,
639 bootc.status = "importing_from_storage",
640 "Importing image from bootc storage: {}",
641 ostree_imgref
642 );
643
644 let prepared_image = PreparedImportMeta {
645 imp,
646 n_layers_to_fetch: layers_to_fetch.len(),
647 layers_total: prep.all_layers().count(),
648 bytes_to_fetch: layers_to_fetch.iter().map(|(l, _)| l.layer.size()).sum(),
649 bytes_total: prep.all_layers().map(|l| l.layer.size()).sum(),
650 digest: prep.manifest_digest.clone(),
651 prep,
652 };
653
654 Ok(PreparedPullResult::Ready(Box::new(prepared_image)))
655}
656
657pub(crate) async fn pull_unified(
659 repo: &ostree::Repo,
660 imgref: &ImageReference,
661 target_imgref: Option<&OstreeImageReference>,
662 quiet: bool,
663 prog: ProgressWriter,
664 store: &Storage,
665 booted_deployment: Option<&ostree::Deployment>,
666) -> Result<Box<ImageState>> {
667 match prepare_for_pull_unified(repo, imgref, target_imgref, store, booted_deployment).await? {
668 PreparedPullResult::AlreadyPresent(existing) => {
669 const IMAGE_ALREADY_PRESENT_ID: &str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9";
671 tracing::debug!(
672 message_id = IMAGE_ALREADY_PRESENT_ID,
673 bootc.image.reference = &imgref.image,
674 bootc.image.transport = &imgref.transport,
675 bootc.status = "already_present",
676 "Image already present: {}",
677 imgref
678 );
679 Ok(existing)
680 }
681 PreparedPullResult::Ready(prepared_image_meta) => {
682 check_disk_space_unified(
683 store.get_ensure_composefs()?.as_ref(),
684 &prepared_image_meta,
685 imgref,
686 )?;
687 let cs_imgref = ImageReference {
689 transport: "containers-storage".to_string(),
690 image: imgref.image.clone(),
691 signature: imgref.signature.clone(),
692 };
693 pull_from_prepared(&cs_imgref, quiet, prog, *prepared_image_meta).await
694 }
695 }
696}
697
698#[context("Pulling")]
699pub(crate) async fn pull_from_prepared(
700 imgref: &ImageReference,
701 quiet: bool,
702 prog: ProgressWriter,
703 mut prepared_image: PreparedImportMeta,
704) -> Result<Box<ImageState>> {
705 let layer_progress = prepared_image.imp.request_progress();
706 let layer_byte_progress = prepared_image.imp.request_layer_progress();
707 let digest = prepared_image.digest.clone();
708 let digest_imp = prepared_image.digest.clone();
709
710 let printer = tokio::task::spawn(async move {
711 handle_layer_progress_print(LayerProgressConfig {
712 layers: layer_progress,
713 layer_bytes: layer_byte_progress,
714 digest: digest.as_ref().into(),
715 n_layers_to_fetch: prepared_image.n_layers_to_fetch,
716 layers_total: prepared_image.layers_total,
717 bytes_to_download: prepared_image.bytes_to_fetch,
718 bytes_total: prepared_image.bytes_total,
719 prog,
720 quiet,
721 })
722 .await
723 });
724 let import = prepared_image.imp.import(prepared_image.prep).await;
725 let prog = printer.await?;
726 prog.send(Event::ProgressSteps {
728 task: "importing".into(),
729 description: "Importing Image".into(),
730 id: digest_imp.clone().as_ref().into(),
731 steps_cached: 0,
732 steps: 1,
733 steps_total: 1,
734 subtasks: [SubTaskStep {
735 subtask: "importing".into(),
736 description: "Importing Image".into(),
737 id: "importing".into(),
738 completed: true,
739 }]
740 .into(),
741 })
742 .await;
743 let import = import?;
744 let imgref_canonicalized = imgref.clone().canonicalize()?;
745 tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
746
747 let is_unified_path = imgref.transport == "containers-storage";
749 if !is_unified_path {
750 const IMPORT_COMPLETE_JOURNAL_ID: &str = "4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8";
751
752 tracing::info!(
753 message_id = IMPORT_COMPLETE_JOURNAL_ID,
754 bootc.image.reference = &imgref.image,
755 bootc.image.transport = &imgref.transport,
756 bootc.manifest_digest = import.manifest_digest.as_ref(),
757 bootc.ostree_commit = &import.merge_commit,
758 "Successfully imported image: {}",
759 imgref
760 );
761 }
762
763 if let Some(msg) =
764 ostree_container::store::image_filtered_content_warning(&import.filtered_files)
765 .context("Image content warning")?
766 {
767 tracing::info!("{}", msg);
768 }
769 Ok(Box::new((*import).into()))
770}
771
772pub(crate) async fn pull(
774 repo: &ostree::Repo,
775 imgref: &ImageReference,
776 target_imgref: Option<&OstreeImageReference>,
777 quiet: bool,
778 prog: ProgressWriter,
779 booted_deployment: Option<&ostree::Deployment>,
780) -> Result<Box<ImageState>> {
781 match prepare_for_pull(repo, imgref, target_imgref, booted_deployment).await? {
782 PreparedPullResult::AlreadyPresent(existing) => {
783 const IMAGE_ALREADY_PRESENT_ID: &str = "5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9";
785 tracing::debug!(
786 message_id = IMAGE_ALREADY_PRESENT_ID,
787 bootc.image.reference = &imgref.image,
788 bootc.image.transport = &imgref.transport,
789 bootc.status = "already_present",
790 "Image already present: {}",
791 imgref
792 );
793 Ok(existing)
794 }
795 PreparedPullResult::Ready(prepared_image_meta) => {
796 check_disk_space_ostree(repo, &prepared_image_meta, imgref)?;
798 const PULLING_NEW_IMAGE_ID: &str = "6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0";
800 tracing::info!(
801 message_id = PULLING_NEW_IMAGE_ID,
802 bootc.image.reference = &imgref.image,
803 bootc.image.transport = &imgref.transport,
804 bootc.status = "pulling_new",
805 "Pulling new image: {}",
806 imgref
807 );
808 Ok(pull_from_prepared(imgref, quiet, prog, *prepared_image_meta).await?)
809 }
810 }
811}
812
813pub(crate) async fn wipe_ostree(sysroot: Sysroot) -> Result<()> {
814 tokio::task::spawn_blocking(move || {
815 sysroot
816 .write_deployments(&[], gio::Cancellable::NONE)
817 .context("removing deployments")
818 })
819 .await??;
820
821 Ok(())
822}
823
824pub(crate) async fn cleanup(sysroot: &Storage) -> Result<()> {
825 const CLEANUP_JOURNAL_ID: &str = "2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6";
827
828 tracing::info!(
829 message_id = CLEANUP_JOURNAL_ID,
830 "Starting cleanup of old images and deployments"
831 );
832
833 let bound_prune = prune_container_store(sysroot);
834
835 let ostree = sysroot.get_ostree_cloned()?;
837 let repo = ostree.repo();
838 let repo_prune =
839 ostree_ext::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
840 let locked_sysroot = &SysrootLock::from_assumed_locked(&ostree);
841 let cancellable = Some(cancellable);
842 let repo = &repo;
843 let txn = repo.auto_transaction(cancellable)?;
844 let repo = txn.repo();
845
846 for ref_entry in repo
848 .list_refs_ext(
849 Some(BASE_IMAGE_PREFIX),
850 ostree::RepoListRefsExtFlags::NONE,
851 cancellable,
852 )
853 .context("Listing refs")?
854 .keys()
855 {
856 repo.transaction_set_refspec(ref_entry, None);
857 }
858
859 for (i, deployment) in ostree.deployments().into_iter().enumerate() {
862 let commit = deployment.csum();
863 if let Some(base) = get_base_commit(repo, &commit)? {
864 repo.transaction_set_refspec(&format!("{BASE_IMAGE_PREFIX}/{i}"), Some(&base));
865 }
866 }
867
868 let pruned =
869 ostree_container::deploy::prune(locked_sysroot).context("Pruning images")?;
870 if !pruned.is_empty() {
871 let size = glib::format_size(pruned.objsize);
872 println!(
873 "Pruned images: {} (layers: {}, objsize: {})",
874 pruned.n_images, pruned.n_layers, size
875 );
876 } else {
877 tracing::debug!("Nothing to prune");
878 }
879
880 Ok(())
881 });
882
883 tokio::try_join!(repo_prune, bound_prune)?;
885 Ok(())
886}
887
888#[context("Finding base commit")]
890pub(crate) fn get_base_commit(repo: &ostree::Repo, commit: &str) -> Result<Option<String>> {
891 let commitv = repo.load_commit(commit)?.0;
892 let commitmeta = commitv.child_value(0);
893 let commitmeta = &glib::VariantDict::new(Some(&commitmeta));
894 let r = commitmeta.lookup::<String>(BOOTC_DERIVED_KEY)?;
895 Ok(r)
896}
897
898#[context("Writing deployment")]
899async fn deploy(
900 sysroot: &Storage,
901 from: MergeState,
902 image: &ImageState,
903 origin: &glib::KeyFile,
904 lock_finalization: bool,
905) -> Result<Deployment> {
906 let (stateroot, override_kargs) = match &from {
911 MergeState::MergeDeployment(deployment) => {
912 let kargs = crate::bootc_kargs::get_kargs(sysroot, &deployment, image)?;
913 (deployment.stateroot().into(), Some(kargs))
914 }
915 MergeState::Reset { stateroot, kargs } => (stateroot.clone(), Some(kargs.clone())),
916 };
917 let ostree = sysroot.get_ostree_cloned()?;
919 let merge_deployment = from.as_merge_deployment();
921 let merge_deployment = merge_deployment.map(|d| d.index() as usize);
922 let ostree_commit = image.ostree_commit.to_string();
923 let origin_data = origin.to_data();
925 let r = async_task_with_spinner(
926 "Deploying",
927 spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
928 let ostree = ostree;
929 let stateroot = Some(stateroot);
930 let mut opts = ostree::SysrootDeployTreeOpts::default();
931
932 opts.locked = lock_finalization;
934
935 let override_kargs_refs = override_kargs
938 .as_ref()
939 .map(|kargs| kargs.iter_str().collect::<Vec<_>>());
940 if let Some(kargs) = override_kargs_refs.as_ref() {
941 opts.override_kernel_argv = Some(kargs);
942 }
943
944 let deployments = ostree.deployments();
945 let merge_deployment = merge_deployment.map(|m| &deployments[m]);
946 let origin = glib::KeyFile::new();
947 origin.load_from_data(&origin_data, glib::KeyFileFlags::NONE)?;
948 let d = ostree.stage_tree_with_options(
949 stateroot.as_deref(),
950 &ostree_commit,
951 Some(&origin),
952 merge_deployment,
953 &opts,
954 Some(cancellable),
955 )?;
956 Ok(d.index())
957 }),
958 )
959 .await?;
960 let ostree = sysroot.get_ostree()?;
962 let staged = ostree.staged_deployment().unwrap();
963 assert_eq!(staged.index(), r);
964 Ok(staged)
965}
966
967#[context("Generating origin")]
968fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
969 let origin = glib::KeyFile::new();
970 let imgref = OstreeImageReference::from(imgref.clone());
971 origin.set_string(
972 "origin",
973 ostree_container::deploy::ORIGIN_CONTAINER,
974 imgref.to_string().as_str(),
975 );
976 Ok(origin)
977}
978
979#[derive(Debug)]
981pub(crate) enum MergeState {
982 MergeDeployment(Deployment),
984 Reset {
987 stateroot: String,
988 kargs: CmdlineOwned,
989 },
990}
991impl MergeState {
992 pub(crate) fn from_stateroot(sysroot: &Storage, stateroot: &str) -> Result<Self> {
994 let ostree = sysroot.get_ostree()?;
995 let merge_deployment = ostree.merge_deployment(Some(stateroot)).ok_or_else(|| {
996 anyhow::anyhow!("No merge deployment found for stateroot {stateroot}")
997 })?;
998 Ok(Self::MergeDeployment(merge_deployment))
999 }
1000
1001 pub(crate) fn as_merge_deployment(&self) -> Option<&Deployment> {
1003 match self {
1004 Self::MergeDeployment(d) => Some(d),
1005 Self::Reset { .. } => None,
1006 }
1007 }
1008}
1009
1010#[context("Staging")]
1012pub(crate) async fn stage(
1013 sysroot: &Storage,
1014 from: MergeState,
1015 image: &ImageState,
1016 spec: &RequiredHostSpec<'_>,
1017 prog: ProgressWriter,
1018 lock_finalization: bool,
1019) -> Result<()> {
1020 const STAGE_JOURNAL_ID: &str = "8f7a2b1c3d4e5f6a7b8c9d0e1f2a3b4c";
1022
1023 tracing::info!(
1024 message_id = STAGE_JOURNAL_ID,
1025 bootc.image.reference = &spec.image.image,
1026 bootc.image.transport = &spec.image.transport,
1027 bootc.manifest_digest = image.manifest_digest.as_ref(),
1028 "Staging image for deployment: {} (digest: {})",
1029 spec.image,
1030 image.manifest_digest
1031 );
1032
1033 let mut subtask = SubTaskStep {
1034 subtask: "merging".into(),
1035 description: "Merging Image".into(),
1036 id: "fetching".into(),
1037 completed: false,
1038 };
1039 let mut subtasks = vec![];
1040 prog.send(Event::ProgressSteps {
1041 task: "staging".into(),
1042 description: "Deploying Image".into(),
1043 id: image.manifest_digest.clone().as_ref().into(),
1044 steps_cached: 0,
1045 steps: 0,
1046 steps_total: 3,
1047 subtasks: subtasks
1048 .clone()
1049 .into_iter()
1050 .chain([subtask.clone()])
1051 .collect(),
1052 })
1053 .await;
1054
1055 subtask.completed = true;
1056 subtasks.push(subtask.clone());
1057 subtask.subtask = "deploying".into();
1058 subtask.id = "deploying".into();
1059 subtask.description = "Deploying Image".into();
1060 subtask.completed = false;
1061 prog.send(Event::ProgressSteps {
1062 task: "staging".into(),
1063 description: "Deploying Image".into(),
1064 id: image.manifest_digest.clone().as_ref().into(),
1065 steps_cached: 0,
1066 steps: 1,
1067 steps_total: 3,
1068 subtasks: subtasks
1069 .clone()
1070 .into_iter()
1071 .chain([subtask.clone()])
1072 .collect(),
1073 })
1074 .await;
1075 let origin = origin_from_imageref(spec.image)?;
1076 let deployment =
1077 crate::deploy::deploy(sysroot, from, image, &origin, lock_finalization).await?;
1078
1079 subtask.completed = true;
1080 subtasks.push(subtask.clone());
1081 subtask.subtask = "bound_images".into();
1082 subtask.id = "bound_images".into();
1083 subtask.description = "Pulling Bound Images".into();
1084 subtask.completed = false;
1085 prog.send(Event::ProgressSteps {
1086 task: "staging".into(),
1087 description: "Deploying Image".into(),
1088 id: image.manifest_digest.clone().as_ref().into(),
1089 steps_cached: 0,
1090 steps: 1,
1091 steps_total: 3,
1092 subtasks: subtasks
1093 .clone()
1094 .into_iter()
1095 .chain([subtask.clone()])
1096 .collect(),
1097 })
1098 .await;
1099 crate::boundimage::pull_bound_images(sysroot, &deployment).await?;
1100
1101 subtask.completed = true;
1102 subtasks.push(subtask.clone());
1103 subtask.subtask = "cleanup".into();
1104 subtask.id = "cleanup".into();
1105 subtask.description = "Removing old images".into();
1106 subtask.completed = false;
1107 prog.send(Event::ProgressSteps {
1108 task: "staging".into(),
1109 description: "Deploying Image".into(),
1110 id: image.manifest_digest.clone().as_ref().into(),
1111 steps_cached: 0,
1112 steps: 2,
1113 steps_total: 3,
1114 subtasks: subtasks
1115 .clone()
1116 .into_iter()
1117 .chain([subtask.clone()])
1118 .collect(),
1119 })
1120 .await;
1121 crate::deploy::cleanup(sysroot).await?;
1122 println!("Queued for next boot: {:#}", spec.image);
1123 if let Some(version) = image.version.as_deref() {
1124 println!(" Version: {version}");
1125 }
1126 println!(" Digest: {}", image.manifest_digest);
1127
1128 subtask.completed = true;
1129 subtasks.push(subtask.clone());
1130 prog.send(Event::ProgressSteps {
1131 task: "staging".into(),
1132 description: "Deploying Image".into(),
1133 id: image.manifest_digest.clone().as_ref().into(),
1134 steps_cached: 0,
1135 steps: 3,
1136 steps_total: 3,
1137 subtasks: subtasks
1138 .clone()
1139 .into_iter()
1140 .chain([subtask.clone()])
1141 .collect(),
1142 })
1143 .await;
1144
1145 write_reboot_required(&image.manifest_digest.as_ref())?;
1148
1149 Ok(())
1150}
1151
1152fn write_reboot_required(image: &str) -> Result<()> {
1154 let reboot_message = format!("bootc: Reboot required for image: {}", image);
1155 let run_dir = Dir::open_ambient_dir("/run", cap_std::ambient_authority())?;
1156 run_dir
1157 .atomic_write("reboot-required", reboot_message.as_bytes())
1158 .context("Creating /run/reboot-required")?;
1159
1160 Ok(())
1161}
1162
1163pub(crate) const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468";
1164
1165pub(crate) async fn rollback(sysroot: &Storage) -> Result<()> {
1167 let ostree = sysroot.get_ostree()?;
1168 let (booted_ostree, deployments, host) = crate::status::get_status_require_booted(ostree)?;
1169
1170 let new_spec = {
1171 let mut new_spec = host.spec.clone();
1172 new_spec.boot_order = new_spec.boot_order.swap();
1173 new_spec
1174 };
1175
1176 let repo = &booted_ostree.repo();
1177
1178 host.spec.verify_transition(&new_spec)?;
1180
1181 let reverting = new_spec.boot_order == BootOrder::Default;
1182 if reverting {
1183 println!("notice: Reverting queued rollback state");
1184 }
1185 let rollback_status = host
1186 .status
1187 .rollback
1188 .ok_or_else(|| anyhow!("No rollback available"))?;
1189 let rollback_image = rollback_status
1190 .query_image(repo)?
1191 .ok_or_else(|| anyhow!("Rollback is not container image based"))?;
1192
1193 let current_image = host
1195 .status
1196 .booted
1197 .as_ref()
1198 .and_then(|b| b.query_image(repo).ok()?);
1199
1200 tracing::info!(
1201 message_id = ROLLBACK_JOURNAL_ID,
1202 bootc.manifest_digest = rollback_image.manifest_digest.as_ref(),
1203 bootc.ostree_commit = &rollback_image.merge_commit,
1204 bootc.rollback_type = if reverting { "revert" } else { "rollback" },
1205 bootc.current_manifest_digest = current_image
1206 .as_ref()
1207 .map(|i| i.manifest_digest.as_ref())
1208 .unwrap_or("none"),
1209 "Rolling back to image: {}",
1210 rollback_image.manifest_digest
1211 );
1212 let rollback_deployment = deployments.rollback.expect("rollback deployment");
1214 let new_deployments = if reverting {
1215 [booted_ostree.deployment, rollback_deployment]
1216 } else {
1217 [rollback_deployment, booted_ostree.deployment]
1218 };
1219 let new_deployments = new_deployments
1220 .into_iter()
1221 .chain(deployments.other)
1222 .collect::<Vec<_>>();
1223 tracing::debug!("Writing new deployments: {new_deployments:?}");
1224 booted_ostree
1225 .sysroot
1226 .write_deployments(&new_deployments, gio::Cancellable::NONE)?;
1227 if reverting {
1228 println!("Next boot: current deployment");
1229 } else {
1230 println!("Next boot: rollback deployment");
1231 }
1232
1233 write_reboot_required(rollback_image.manifest_digest.as_ref())?;
1234
1235 sysroot.update_mtime()?;
1236
1237 Ok(())
1238}
1239
1240fn find_newest_deployment_name(deploysdir: &Dir) -> Result<String> {
1241 let mut dirs = Vec::new();
1242 for ent in deploysdir.entries()? {
1243 let ent = ent?;
1244 if !ent.file_type()?.is_dir() {
1245 continue;
1246 }
1247 let name = ent.file_name();
1248 let Some(name) = name.to_str() else {
1249 continue;
1250 };
1251 dirs.push((name.to_owned(), ent.metadata()?.mtime()));
1252 }
1253 dirs.sort_unstable_by(|a, b| a.1.cmp(&b.1));
1254 if let Some((name, _ts)) = dirs.pop() {
1255 Ok(name)
1256 } else {
1257 anyhow::bail!("No deployment directory found")
1258 }
1259}
1260
1261pub(crate) fn switch_origin_inplace(root: &Dir, imgref: &ImageReference) -> Result<String> {
1263 const SWITCH_INPLACE_JOURNAL_ID: &str = "3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7";
1265
1266 tracing::info!(
1267 message_id = SWITCH_INPLACE_JOURNAL_ID,
1268 bootc.image.reference = &imgref.image,
1269 bootc.image.transport = &imgref.transport,
1270 bootc.switch_type = "in_place",
1271 "Performing in-place switch to image: {}",
1272 imgref
1273 );
1274
1275 let origin = origin_from_imageref(imgref)?;
1277 let serialized_origin = origin.to_data();
1278
1279 let mut ostree_deploys = root.open_dir("sysroot/ostree/deploy")?.entries()?;
1288 let deploydir = loop {
1289 if let Some(ent) = ostree_deploys.next() {
1290 let ent = ent?;
1291 if !ent.file_type()?.is_dir() {
1292 continue;
1293 }
1294 tracing::debug!("Checking {:?}", ent.file_name());
1295 let child_dir = ent
1296 .open_dir()
1297 .with_context(|| format!("Opening dir {:?}", ent.file_name()))?;
1298 if let Some(d) = child_dir.open_dir_optional("deploy")? {
1299 break d;
1300 }
1301 } else {
1302 anyhow::bail!("Failed to find a deployment");
1303 }
1304 };
1305 let newest_deployment = find_newest_deployment_name(&deploydir)?;
1306 let origin_path = format!("{newest_deployment}.origin");
1307 if !deploydir.try_exists(&origin_path)? {
1308 tracing::warn!("No extant origin for {newest_deployment}");
1309 }
1310 deploydir
1311 .atomic_write(&origin_path, serialized_origin.as_bytes())
1312 .context("Writing origin")?;
1313 Ok(newest_deployment)
1314}
1315
1316#[context("Updating /etc/fstab for anaconda+composefs")]
1319pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> {
1320 let fstab_path = "etc/fstab";
1321 let fd = root
1323 .open(fstab_path)
1324 .with_context(|| format!("Opening {fstab_path}"))
1325 .map(std::io::BufReader::new)?;
1326
1327 fn edit_fstab_line(line: &str, mut w: impl Write) -> Result<bool> {
1331 if line.starts_with('#') {
1332 return Ok(false);
1333 }
1334 let parts = line.split_ascii_whitespace().collect::<Vec<_>>();
1335
1336 let path_idx = 1;
1337 let options_idx = 3;
1338 let (&path, &options) = match (parts.get(path_idx), parts.get(options_idx)) {
1339 (None, _) => {
1340 tracing::debug!("No path in entry: {line}");
1341 return Ok(false);
1342 }
1343 (_, None) => {
1344 tracing::debug!("No options in entry: {line}");
1345 return Ok(false);
1346 }
1347 (Some(p), Some(o)) => (p, o),
1348 };
1349 if path != "/" {
1351 return Ok(false);
1352 }
1353 if options.split(',').any(|s| s == "ro") {
1355 return Ok(false);
1356 }
1357
1358 writeln!(w, "# {}", crate::generator::BOOTC_EDITED_STAMP)?;
1359
1360 assert!(!options.is_empty()); let options = format!("{options},ro");
1364 for (i, part) in parts.into_iter().enumerate() {
1365 if i > 0 {
1367 write!(w, " ")?;
1368 }
1369 if i == options_idx {
1370 write!(w, "{options}")?;
1371 } else {
1372 write!(w, "{part}")?
1373 }
1374 }
1375 writeln!(w)?;
1377 Ok(true)
1378 }
1379
1380 root.atomic_replace_with(fstab_path, move |mut w| -> Result<()> {
1382 for line in fd.lines() {
1383 let line = line?;
1384 if !edit_fstab_line(&line, &mut w)? {
1385 writeln!(w, "{line}")?;
1386 }
1387 }
1388 Ok(())
1389 })
1390 .context("Replacing /etc/fstab")?;
1391
1392 println!("Updated /etc/fstab to add `ro` for `/`");
1393 Ok(())
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398 use super::*;
1399
1400 #[test]
1401 fn test_new_proxy_config_user_agent() {
1402 let config = new_proxy_config();
1403 let prefix = config
1404 .user_agent_prefix
1405 .expect("user_agent_prefix should be set");
1406 assert!(
1407 prefix.starts_with("bootc/"),
1408 "User agent should start with bootc/"
1409 );
1410 assert!(
1412 prefix.len() > "bootc/".len(),
1413 "Version should be present after bootc/"
1414 );
1415 }
1416
1417 #[test]
1418 fn test_switch_inplace() -> Result<()> {
1419 use cap_std::fs::DirBuilderExt;
1420
1421 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1422 let mut builder = cap_std::fs::DirBuilder::new();
1423 let builder = builder.recursive(true).mode(0o755);
1424 let deploydir = "sysroot/ostree/deploy/default/deploy";
1425 let target_deployment =
1426 "af36eb0086bb55ac601600478c6168f834288013d60f8870b7851f44bf86c3c5.0";
1427 td.ensure_dir_with(
1428 format!("sysroot/ostree/deploy/default/deploy/{target_deployment}"),
1429 builder,
1430 )?;
1431 let deploydir = &td.open_dir(deploydir)?;
1432 let orig_imgref = ImageReference {
1433 image: "quay.io/exampleos/original:sometag".into(),
1434 transport: "registry".into(),
1435 signature: None,
1436 };
1437 {
1438 let origin = origin_from_imageref(&orig_imgref)?;
1439 deploydir.atomic_write(
1440 format!("{target_deployment}.origin"),
1441 origin.to_data().as_bytes(),
1442 )?;
1443 }
1444
1445 let target_imgref = ImageReference {
1446 image: "quay.io/someother/otherimage:latest".into(),
1447 transport: "registry".into(),
1448 signature: None,
1449 };
1450
1451 let replaced = switch_origin_inplace(&td, &target_imgref).unwrap();
1452 assert_eq!(replaced, target_deployment);
1453 Ok(())
1454 }
1455
1456 #[test]
1457 fn test_fixup_etc_fstab_default() -> Result<()> {
1458 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1459 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n";
1460 tempdir.create_dir_all("etc")?;
1461 tempdir.atomic_write("etc/fstab", default)?;
1462 fixup_etc_fstab(&tempdir).unwrap();
1463 assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
1464 Ok(())
1465 }
1466
1467 #[test]
1468 fn test_fixup_etc_fstab_multi() -> Result<()> {
1469 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1470 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1471UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1472 tempdir.create_dir_all("etc")?;
1473 tempdir.atomic_write("etc/fstab", default)?;
1474 fixup_etc_fstab(&tempdir).unwrap();
1475 assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
1476 Ok(())
1477 }
1478
1479 #[test]
1480 fn test_fixup_etc_fstab_ro() -> Result<()> {
1481 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1482 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1483UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs ro 0 0\n\
1484UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1485 tempdir.create_dir_all("etc")?;
1486 tempdir.atomic_write("etc/fstab", default)?;
1487 fixup_etc_fstab(&tempdir).unwrap();
1488 assert_eq!(tempdir.read_to_string("etc/fstab")?, default);
1489 Ok(())
1490 }
1491
1492 #[test]
1493 fn test_fixup_etc_fstab_rw() -> Result<()> {
1494 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
1495 let default = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1497UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults 0 0\n\
1498UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1499 let modified = "UUID=f7436547-20ac-43cb-aa2f-eac9632183f6 /boot auto ro 0 0\n\
1500# Updated by bootc-fstab-edit.service\n\
1501UUID=1eef9f42-40e3-4bd8-ae20-e9f2325f8b52 / xfs defaults,ro 0 0\n\
1502UUID=6907-17CA /boot/efi vfat umask=0077,shortname=winnt 0 2\n";
1503 tempdir.create_dir_all("etc")?;
1504 tempdir.atomic_write("etc/fstab", default)?;
1505 fixup_etc_fstab(&tempdir).unwrap();
1506 assert_eq!(tempdir.read_to_string("etc/fstab")?, modified);
1507 Ok(())
1508 }
1509 #[test]
1510 fn test_check_disk_space_inner() -> Result<()> {
1511 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
1512 let imgref = ImageReference {
1513 image: "quay.io/exampleos/exampleos:latest".into(),
1514 transport: "registry".into(),
1515 signature: None,
1516 };
1517
1518 check_disk_space_inner(&*td, 0, 0, &imgref)?;
1520
1521 assert!(check_disk_space_inner(&*td, u64::MAX, 0, &imgref).is_err());
1523
1524 assert!(check_disk_space_inner(&*td, 1, u64::MAX, &imgref).is_err());
1526
1527 Ok(())
1528 }
1529}