1use std::fmt::Display;
4
5use std::str::FromStr;
6
7use anyhow::Result;
8use ostree_ext::container::Transport;
9use ostree_ext::oci_spec::distribution::Reference;
10use ostree_ext::oci_spec::image::Digest;
11use ostree_ext::{container::OstreeImageReference, oci_spec, ostree::DeploymentUnlockedState};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14
15use crate::bootc_composefs::boot::BootType;
16use crate::{k8sapitypes, status::Slot};
17
18const API_VERSION: &str = "org.containers.bootc/v1";
19const KIND: &str = "BootcHost";
20pub(crate) const OBJECT_NAME: &str = "host";
22
23#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
24#[serde(rename_all = "camelCase")]
25pub struct Host {
27 #[serde(flatten)]
29 pub resource: k8sapitypes::Resource,
30 #[serde(default)]
32 pub spec: HostSpec,
33 #[serde(default)]
35 pub status: HostStatus,
36}
37
38#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)]
41#[serde(rename_all = "camelCase")]
42pub enum BootOrder {
43 #[default]
45 Default,
46 Rollback,
48}
49
50#[derive(
51 clap::ValueEnum, Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, Default,
52)]
53#[serde(rename_all = "camelCase")]
54pub enum Store {
56 #[default]
58 #[value(alias = "ostreecontainer")] OstreeContainer,
60}
61
62#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)]
63#[serde(rename_all = "camelCase")]
64pub struct HostSpec {
66 pub image: Option<ImageReference>,
68 #[serde(default)]
70 pub boot_order: BootOrder,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
74#[serde(rename_all = "camelCase")]
76pub enum ImageSignature {
77 OstreeRemote(String),
79 ContainerPolicy,
81 Insecure,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
87#[serde(rename_all = "camelCase")]
88pub struct ImageReference {
89 pub image: String,
91 pub transport: String,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub signature: Option<ImageSignature>,
96}
97
98fn canonicalize_reference(reference: Reference) -> Option<Reference> {
100 reference.tag()?;
102
103 let digest = reference.digest()?;
105 Some(reference.clone_with_digest(digest.to_owned()))
107}
108
109impl ImageReference {
110 pub fn canonicalize(self) -> Result<Self> {
112 let transport = Transport::try_from(self.transport.as_str())?;
114 match transport {
115 Transport::Registry => {
116 let reference: oci_spec::distribution::Reference = self.image.parse()?;
117
118 let Some(reference) = canonicalize_reference(reference) else {
120 return Ok(self);
121 };
122
123 let r = ImageReference {
124 image: reference.to_string(),
125 transport: self.transport.clone(),
126 signature: self.signature.clone(),
127 };
128 Ok(r)
129 }
130 _ => {
131 Ok(self)
133 }
134 }
135 }
136
137 pub fn transport(&self) -> Result<Transport> {
139 Transport::try_from(self.transport.as_str())
140 .map_err(|e| anyhow::anyhow!("Invalid transport '{}': {}", self.transport, e))
141 }
142
143 pub fn to_image_proxy_ref(&self) -> Result<ostree_ext::containers_image_proxy::ImageReference> {
148 let s = format!("{}:{}", self.transport, self.image);
149 s.as_str()
150 .try_into()
151 .map_err(|e| anyhow::anyhow!("Parsing image reference '{}': {}", s, e))
152 }
153
154 pub fn to_transport_image(&self) -> Result<String> {
157 if self.transport()? == Transport::Registry {
158 Ok(self.image.clone())
160 } else {
161 Ok(format!("{}:{}", self.transport, self.image))
163 }
164 }
165
166 pub fn with_tag(&self, new_tag: &str) -> Result<Self> {
172 let new_image = if let Ok(reference) = self.image.parse::<Reference>() {
174 let new_ref = Reference::with_tag(
176 reference.registry().to_string(),
177 reference.repository().to_string(),
178 new_tag.to_string(),
179 );
180 new_ref.to_string()
181 } else {
182 let image_without_digest = self.image.split('@').next().unwrap_or(&self.image);
185
186 let image_part = image_without_digest
188 .rsplit_once(':')
189 .map(|(base, _tag)| base)
190 .unwrap_or(image_without_digest);
191
192 format!("{}:{}", image_part, new_tag)
193 };
194
195 Ok(ImageReference {
196 image: new_image,
197 transport: self.transport.clone(),
198 signature: self.signature.clone(),
199 })
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
205#[serde(rename_all = "camelCase")]
206pub struct ImageStatus {
207 pub image: ImageReference,
209 pub version: Option<String>,
211 pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
213 pub image_digest: String,
215 pub architecture: String,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
221#[serde(rename_all = "camelCase")]
222pub struct BootEntryOstree {
223 pub stateroot: String,
225 pub checksum: String,
227 pub deploy_serial: u32,
229}
230
231#[derive(
233 clap::ValueEnum, Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema,
234)]
235#[serde(rename_all = "kebab-case")]
236pub enum Bootloader {
237 #[default]
239 Grub,
240 Systemd,
242 None,
244}
245
246impl Display for Bootloader {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 let string = match self {
249 Bootloader::Grub => "grub",
250 Bootloader::Systemd => "systemd",
251 Bootloader::None => "none",
252 };
253
254 write!(f, "{}", string)
255 }
256}
257
258impl FromStr for Bootloader {
259 type Err = anyhow::Error;
260
261 fn from_str(value: &str) -> Result<Self> {
262 match value {
263 "grub" => Ok(Self::Grub),
264 "systemd" => Ok(Self::Systemd),
265 "none" => Ok(Self::None),
266 unrecognized => Err(anyhow::anyhow!("Unrecognized bootloader: '{unrecognized}'")),
267 }
268 }
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
273#[serde(rename_all = "camelCase")]
274pub struct BootEntryComposefs {
275 pub verity: String,
277 pub boot_type: BootType,
279 pub bootloader: Bootloader,
281 pub boot_digest: Option<String>,
284 pub missing_verity_allowed: bool,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
290#[serde(rename_all = "camelCase")]
291pub struct BootEntry {
292 pub image: Option<ImageStatus>,
294 pub cached_update: Option<ImageStatus>,
296 pub incompatible: bool,
298 pub pinned: bool,
300 #[serde(default)]
302 pub soft_reboot_capable: bool,
303 #[serde(default)]
306 pub download_only: bool,
307 #[serde(default)]
309 pub store: Option<Store>,
310 pub ostree: Option<BootEntryOstree>,
312 pub composefs: Option<BootEntryComposefs>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
317#[serde(rename_all = "camelCase")]
318#[non_exhaustive]
319pub enum HostType {
322 BootcHost,
324}
325
326#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema)]
328#[serde(rename_all = "camelCase")]
329pub struct FilesystemOverlay {
330 pub access_mode: FilesystemOverlayAccessMode,
332 pub persistence: FilesystemOverlayPersistence,
334}
335
336#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema)]
338#[serde(rename_all = "camelCase")]
339pub enum FilesystemOverlayAccessMode {
340 ReadOnly,
342 ReadWrite,
344}
345
346impl Display for FilesystemOverlayAccessMode {
347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 match self {
349 FilesystemOverlayAccessMode::ReadOnly => write!(f, "read-only"),
350 FilesystemOverlayAccessMode::ReadWrite => write!(f, "read/write"),
351 }
352 }
353}
354
355#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema)]
357#[serde(rename_all = "camelCase")]
358pub enum FilesystemOverlayPersistence {
359 Transient,
361 Persistent,
363}
364
365impl Display for FilesystemOverlayPersistence {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 match self {
368 FilesystemOverlayPersistence::Transient => write!(f, "transient"),
369 FilesystemOverlayPersistence::Persistent => write!(f, "persistent"),
370 }
371 }
372}
373
374pub(crate) fn deployment_unlocked_state_to_usr_overlay(
375 state: DeploymentUnlockedState,
376) -> Option<FilesystemOverlay> {
377 use FilesystemOverlayAccessMode::*;
378 use FilesystemOverlayPersistence::*;
379 match state {
380 DeploymentUnlockedState::None => None,
381 DeploymentUnlockedState::Development => Some(FilesystemOverlay {
382 access_mode: ReadWrite,
383 persistence: Transient,
384 }),
385 DeploymentUnlockedState::Hotfix => Some(FilesystemOverlay {
386 access_mode: ReadWrite,
387 persistence: Persistent,
388 }),
389 DeploymentUnlockedState::Transient => Some(FilesystemOverlay {
390 access_mode: ReadOnly,
391 persistence: Transient,
392 }),
393 _ => None,
394 }
395}
396
397impl Display for FilesystemOverlay {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 write!(f, "{}, {}", self.persistence, self.access_mode)
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq, Eq, JsonSchema)]
405#[serde(rename_all = "camelCase")]
406pub struct HostStatus {
407 pub staged: Option<BootEntry>,
409 pub booted: Option<BootEntry>,
411 pub rollback: Option<BootEntry>,
413 #[serde(skip_serializing_if = "Vec::is_empty")]
415 #[serde(default)]
416 pub other_deployments: Vec<BootEntry>,
417 #[serde(default)]
419 pub rollback_queued: bool,
420
421 #[serde(rename = "type")]
423 pub ty: Option<HostType>,
424
425 pub usr_overlay: Option<FilesystemOverlay>,
427}
428
429pub(crate) struct DeploymentEntry<'a> {
430 pub(crate) ty: Option<Slot>,
431 pub(crate) deployment: &'a BootEntryComposefs,
432 pub(crate) pinned: bool,
433 pub(crate) soft_reboot_capable: bool,
434}
435
436#[derive(Debug, Serialize)]
438#[serde(rename_all = "kebab-case")]
439pub(crate) struct ContainerInspect {
440 pub(crate) kargs: Vec<String>,
442 pub(crate) kernel: Option<crate::kernel::Kernel>,
444}
445
446impl Host {
447 pub fn new(spec: HostSpec) -> Self {
449 let metadata = k8sapitypes::ObjectMeta {
450 name: Some(OBJECT_NAME.to_owned()),
451 ..Default::default()
452 };
453 Self {
454 resource: k8sapitypes::Resource {
455 api_version: API_VERSION.to_owned(),
456 kind: KIND.to_owned(),
457 metadata,
458 },
459 spec,
460 status: Default::default(),
461 }
462 }
463
464 pub fn filter_to_slot(&mut self, slot: Slot) {
466 match slot {
467 Slot::Staged => {
468 self.status.booted = None;
469 self.status.rollback = None;
470 }
471 Slot::Booted => {
472 self.status.staged = None;
473 self.status.rollback = None;
474 }
475 Slot::Rollback => {
476 self.status.staged = None;
477 self.status.booted = None;
478 }
479 }
480 }
481
482 pub(crate) fn list_deployments(&self) -> Vec<&BootEntry> {
484 self.status
485 .staged
486 .iter()
487 .chain(self.status.booted.iter())
488 .chain(self.status.rollback.iter())
489 .chain(self.status.other_deployments.iter())
490 .collect::<Vec<_>>()
491 }
492
493 pub(crate) fn require_composefs_booted(&self) -> anyhow::Result<&BootEntryComposefs> {
494 let cfs = self
495 .status
496 .booted
497 .as_ref()
498 .ok_or(anyhow::anyhow!("Could not find booted deployment"))?
499 .require_composefs()?;
500
501 Ok(cfs)
502 }
503
504 #[fn_error_context::context("Getting all composefs deployments")]
506 pub(crate) fn all_composefs_deployments<'a>(&'a self) -> Result<Vec<DeploymentEntry<'a>>> {
507 let mut all_deps = vec![];
508
509 let booted = self.require_composefs_booted()?;
510 all_deps.push(DeploymentEntry {
511 ty: Some(Slot::Booted),
512 deployment: booted,
513 pinned: false,
514 soft_reboot_capable: false,
515 });
516
517 if let Some(staged) = &self.status.staged {
518 all_deps.push(DeploymentEntry {
519 ty: Some(Slot::Staged),
520 deployment: staged.require_composefs()?,
521 pinned: false,
522 soft_reboot_capable: staged.soft_reboot_capable,
523 });
524 }
525
526 if let Some(rollback) = &self.status.rollback {
527 all_deps.push(DeploymentEntry {
528 ty: Some(Slot::Rollback),
529 deployment: rollback.require_composefs()?,
530 pinned: false,
531 soft_reboot_capable: rollback.soft_reboot_capable,
532 });
533 }
534
535 for pinned in &self.status.other_deployments {
536 all_deps.push(DeploymentEntry {
537 ty: None,
538 deployment: pinned.require_composefs()?,
539 pinned: true,
540 soft_reboot_capable: pinned.soft_reboot_capable,
541 });
542 }
543
544 Ok(all_deps)
545 }
546}
547
548impl Default for Host {
549 fn default() -> Self {
550 Self::new(Default::default())
551 }
552}
553
554impl HostSpec {
555 pub(crate) fn verify_transition(&self, new: &Self) -> anyhow::Result<()> {
558 let rollback = self.boot_order != new.boot_order;
559 let image_change = self.image != new.image;
560 if rollback && image_change {
561 anyhow::bail!("Invalid state transition: rollback and image change");
562 }
563 Ok(())
564 }
565}
566
567impl BootOrder {
568 pub(crate) fn swap(&self) -> Self {
569 match self {
570 BootOrder::Default => BootOrder::Rollback,
571 BootOrder::Rollback => BootOrder::Default,
572 }
573 }
574}
575
576impl Display for ImageReference {
577 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578 if f.alternate() && self.signature.is_none() && self.transport == "registry" {
580 self.image.fmt(f)
581 } else {
582 let ostree_imgref = OstreeImageReference::from(self.clone());
583 ostree_imgref.fmt(f)
584 }
585 }
586}
587
588impl ImageStatus {
589 pub(crate) fn digest(&self) -> anyhow::Result<Digest> {
590 use std::str::FromStr;
591 Ok(Digest::from_str(&self.image_digest)?)
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use std::str::FromStr;
598
599 use super::*;
600
601 #[test]
602 fn test_canonicalize_reference() {
603 let passthrough = [
605 ("quay.io/example/someimage:latest"),
606 ("quay.io/example/someimage"),
607 ("quay.io/example/someimage@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2"),
608 ];
609 let mapped = [
610 (
611 "quay.io/example/someimage:latest@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
612 "quay.io/example/someimage@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
613 ),
614 (
615 "localhost/someimage:latest@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
616 "localhost/someimage@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
617 ),
618 ];
619 for &v in passthrough.iter() {
620 let reference = Reference::from_str(v).unwrap();
621 assert!(reference.tag().is_none() || reference.digest().is_none());
622 assert!(canonicalize_reference(reference).is_none());
623 }
624 for &(initial, expected) in mapped.iter() {
625 let reference = Reference::from_str(initial).unwrap();
626 assert!(reference.tag().is_some());
627 assert!(reference.digest().is_some());
628 let canonicalized = canonicalize_reference(reference).unwrap();
629 assert_eq!(canonicalized.to_string(), expected);
630 }
631 }
632
633 #[test]
634 fn test_image_reference_canonicalize() {
635 let sample_digest =
636 "sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2";
637
638 let test_cases = [
639 (
641 format!("quay.io/example/someimage:latest@{sample_digest}"),
642 format!("quay.io/example/someimage@{sample_digest}"),
643 "registry",
644 ),
645 (
647 format!("quay.io/example/someimage@{sample_digest}"),
648 format!("quay.io/example/someimage@{sample_digest}"),
649 "registry",
650 ),
651 (
653 "quay.io/example/someimage:latest".to_string(),
654 "quay.io/example/someimage:latest".to_string(),
655 "registry",
656 ),
657 (
659 "quay.io/example/someimage".to_string(),
660 "quay.io/example/someimage".to_string(),
661 "registry",
662 ),
663 (
666 "localhost/someimage:latest".to_string(),
667 "localhost/someimage:latest".to_string(),
668 "registry",
669 ),
670 (
671 format!("localhost/someimage:latest@{sample_digest}"),
672 format!("localhost/someimage@{sample_digest}"),
673 "registry",
674 ),
675 (
677 format!("quay.io/example/someimage:latest@{sample_digest}"),
678 format!("quay.io/example/someimage:latest@{sample_digest}"),
679 "containers-storage",
680 ),
681 (
682 "/path/to/dir:latest".to_string(),
683 "/path/to/dir:latest".to_string(),
684 "oci",
685 ),
686 (
687 "/tmp/repo".to_string(),
688 "/tmp/repo".to_string(),
689 "oci-archive",
690 ),
691 (
692 "/tmp/image-dir".to_string(),
693 "/tmp/image-dir".to_string(),
694 "dir",
695 ),
696 ];
697
698 for (initial, expected, transport) in test_cases {
699 let imgref = ImageReference {
700 image: initial.to_string(),
701 transport: transport.to_string(),
702 signature: None,
703 };
704
705 let canonicalized = imgref.canonicalize();
706 if let Err(e) = canonicalized {
707 panic!("Failed to canonicalize {initial} with transport {transport}: {e}");
708 }
709 let canonicalized = canonicalized.unwrap();
710 assert_eq!(
711 canonicalized.image, expected,
712 "Mismatch for transport {transport}"
713 );
714 assert_eq!(canonicalized.transport, transport);
715 assert_eq!(canonicalized.signature, None);
716 }
717 }
718
719 #[test]
720 fn test_to_image_proxy_ref() {
721 use ostree_ext::containers_image_proxy;
722
723 let cases = [
724 (
725 "registry",
726 "quay.io/example/image:latest",
727 containers_image_proxy::Transport::Registry,
728 "quay.io/example/image:latest",
729 ),
730 (
731 "containers-storage",
732 "localhost/bootc",
733 containers_image_proxy::Transport::ContainerStorage,
734 "localhost/bootc",
735 ),
736 (
737 "oci",
738 "/var/tmp/bootc-oci",
739 containers_image_proxy::Transport::OciDir,
740 "/var/tmp/bootc-oci",
741 ),
742 (
743 "docker-daemon",
744 "myimage:tag",
745 containers_image_proxy::Transport::DockerDaemon,
746 "myimage:tag",
747 ),
748 ];
749
750 for (transport, image, expected_transport, expected_name) in cases {
751 let imgref = ImageReference {
752 transport: transport.to_string(),
753 image: image.to_string(),
754 signature: None,
755 };
756 let proxy_ref = imgref.to_image_proxy_ref().unwrap();
757 assert_eq!(
758 proxy_ref.transport, expected_transport,
759 "transport mismatch for {transport}:{image}"
760 );
761 assert_eq!(
762 proxy_ref.name, expected_name,
763 "name mismatch for {transport}:{image}"
764 );
765 }
766 }
767
768 #[test]
769 fn test_unimplemented_oci_tagged_digested() {
770 let imgref = ImageReference {
771 image: "path/to/image:sometag@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2".to_string(),
772 transport: "oci".to_string(),
773 signature: None
774 };
775 let canonicalized = imgref.clone().canonicalize().unwrap();
776 assert_eq!(imgref, canonicalized);
778 }
779
780 #[test]
781 fn test_parse_spec_v1_null() {
782 const SPEC_FIXTURE: &str = include_str!("fixtures/spec-v1-null.json");
783 let host: Host = serde_json::from_str(SPEC_FIXTURE).unwrap();
784 assert_eq!(host.resource.api_version, "org.containers.bootc/v1");
785 }
786
787 #[test]
788 fn test_parse_spec_v1a1_orig() {
789 const SPEC_FIXTURE: &str = include_str!("fixtures/spec-v1a1-orig.yaml");
790 let host: Host = serde_yaml::from_str(SPEC_FIXTURE).unwrap();
791 assert_eq!(
792 host.spec.image.as_ref().unwrap().image.as_str(),
793 "quay.io/example/someimage:latest"
794 );
795 }
796
797 #[test]
798 fn test_parse_spec_v1a1() {
799 const SPEC_FIXTURE: &str = include_str!("fixtures/spec-v1a1.yaml");
800 let host: Host = serde_yaml::from_str(SPEC_FIXTURE).unwrap();
801 assert_eq!(
802 host.spec.image.as_ref().unwrap().image.as_str(),
803 "quay.io/otherexample/otherimage:latest"
804 );
805 assert_eq!(host.spec.image.as_ref().unwrap().signature, None);
806 }
807
808 #[test]
809 fn test_parse_ostreeremote() {
810 const SPEC_FIXTURE: &str = include_str!("fixtures/spec-ostree-remote.yaml");
811 let host: Host = serde_yaml::from_str(SPEC_FIXTURE).unwrap();
812 assert_eq!(
813 host.spec.image.as_ref().unwrap().signature,
814 Some(ImageSignature::OstreeRemote("fedora".into()))
815 );
816 }
817
818 #[test]
819 fn test_display_imgref() {
820 let src = "ostree-unverified-registry:quay.io/example/foo:sometag";
821 let s = OstreeImageReference::from_str(src).unwrap();
822 let s = ImageReference::from(s);
823 let displayed = format!("{s}");
824 assert_eq!(displayed.as_str(), src);
825 assert_eq!(format!("{s:#}"), "quay.io/example/foo:sometag");
827
828 let src = "ostree-remote-image:fedora:docker://quay.io/example/foo:sometag";
829 let s = OstreeImageReference::from_str(src).unwrap();
830 let s = ImageReference::from(s);
831 let displayed = format!("{s}");
832 assert_eq!(displayed.as_str(), src);
833 assert_eq!(format!("{s:#}"), src);
834 }
835
836 #[test]
837 fn test_store_from_str() {
838 use clap::ValueEnum;
839
840 assert!(Store::from_str("Ostree-Container", true).is_ok());
842 assert!(Store::from_str("OstrEeContAiner", true).is_ok());
843 assert!(Store::from_str("invalid", true).is_err());
844 }
845
846 #[test]
847 fn test_host_filter_to_slot() {
848 fn create_host() -> Host {
849 let mut host = Host::default();
850 host.status.staged = Some(default_boot_entry());
851 host.status.booted = Some(default_boot_entry());
852 host.status.rollback = Some(default_boot_entry());
853 host
854 }
855
856 fn default_boot_entry() -> BootEntry {
857 BootEntry {
858 image: None,
859 cached_update: None,
860 incompatible: false,
861 soft_reboot_capable: false,
862 pinned: false,
863 download_only: false,
864 store: None,
865 ostree: None,
866 composefs: None,
867 }
868 }
869
870 fn assert_host_state(
871 host: &Host,
872 staged: Option<BootEntry>,
873 booted: Option<BootEntry>,
874 rollback: Option<BootEntry>,
875 ) {
876 assert_eq!(host.status.staged, staged);
877 assert_eq!(host.status.booted, booted);
878 assert_eq!(host.status.rollback, rollback);
879 }
880
881 let mut host = create_host();
882 host.filter_to_slot(Slot::Staged);
883 assert_host_state(&host, Some(default_boot_entry()), None, None);
884
885 let mut host = create_host();
886 host.filter_to_slot(Slot::Booted);
887 assert_host_state(&host, None, Some(default_boot_entry()), None);
888
889 let mut host = create_host();
890 host.filter_to_slot(Slot::Rollback);
891 assert_host_state(&host, None, None, Some(default_boot_entry()));
892 }
893
894 #[test]
895 fn test_to_transport_image() {
896 let registry_ref = ImageReference {
898 transport: "registry".to_string(),
899 image: "quay.io/example/foo:latest".to_string(),
900 signature: None,
901 };
902 assert_eq!(
903 registry_ref.to_transport_image().unwrap(),
904 "quay.io/example/foo:latest"
905 );
906
907 let storage_ref = ImageReference {
909 transport: "containers-storage".to_string(),
910 image: "localhost/bootc".to_string(),
911 signature: None,
912 };
913 assert_eq!(
914 storage_ref.to_transport_image().unwrap(),
915 "containers-storage:localhost/bootc"
916 );
917
918 let oci_ref = ImageReference {
920 transport: "oci".to_string(),
921 image: "/path/to/image".to_string(),
922 signature: None,
923 };
924 assert_eq!(oci_ref.to_transport_image().unwrap(), "oci:/path/to/image");
925 }
926}