1use anyhow::anyhow;
164use cap_std_ext::cap_std;
165use cap_std_ext::cap_std::fs::Dir;
166use containers_image_proxy::oci_spec;
167use ostree::glib;
168use serde::Serialize;
169
170use std::borrow::Cow;
171use std::collections::HashMap;
172use std::fmt::Debug;
173use std::ops::Deref;
174use std::str::FromStr;
175
176pub const OSTREE_COMMIT_LABEL: &str = "ostree.commit";
178
179pub(crate) const CONTENT_ANNOTATION: &str = "ostree.components";
182pub(crate) const COMPONENT_SEPARATOR: char = ',';
184
185type Result<T> = anyhow::Result<T>;
188
189#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq)]
191pub enum Transport {
192 Registry,
194 OciDir,
196 OciArchive,
198 DockerArchive,
200 ContainerStorage,
202 Dir,
204 DockerDaemon,
206}
207
208#[derive(Debug, Clone, Hash, PartialEq, Eq)]
212pub struct ImageReference {
213 pub transport: Transport,
215 pub name: String,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Hash)]
221pub enum SignatureSource {
222 OstreeRemote(String),
224 ContainerPolicy,
226 ContainerPolicyAllowInsecure,
228}
229
230pub const LABEL_VERSION: &str = "version";
232
233#[derive(Debug, Clone, PartialEq, Eq, Hash)]
236pub struct OstreeImageReference {
237 pub sigverify: SignatureSource,
239 pub imgref: ImageReference,
241}
242
243impl TryFrom<&str> for Transport {
244 type Error = anyhow::Error;
245
246 fn try_from(value: &str) -> Result<Self> {
247 Ok(match value {
248 Self::REGISTRY_STR | "docker" => Self::Registry,
249 Self::OCI_STR => Self::OciDir,
250 Self::OCI_ARCHIVE_STR => Self::OciArchive,
251 Self::DOCKER_ARCHIVE_STR => Self::DockerArchive,
252 Self::CONTAINERS_STORAGE_STR => Self::ContainerStorage,
253 Self::LOCAL_DIRECTORY_STR => Self::Dir,
254 Self::DOCKER_DAEMON_STR => Self::DockerDaemon,
255 o => return Err(anyhow!("Unknown transport '{}'", o)),
256 })
257 }
258}
259
260impl Transport {
261 const OCI_STR: &'static str = "oci";
262 const OCI_ARCHIVE_STR: &'static str = "oci-archive";
263 const DOCKER_ARCHIVE_STR: &'static str = "docker-archive";
264 const CONTAINERS_STORAGE_STR: &'static str = "containers-storage";
265 const LOCAL_DIRECTORY_STR: &'static str = "dir";
266 const REGISTRY_STR: &'static str = "registry";
267 const DOCKER_DAEMON_STR: &'static str = "docker-daemon";
268
269 pub fn serializable_name(&self) -> &'static str {
271 match self {
272 Transport::Registry => Self::REGISTRY_STR,
273 Transport::OciDir => Self::OCI_STR,
274 Transport::OciArchive => Self::OCI_ARCHIVE_STR,
275 Transport::DockerArchive => Self::DOCKER_ARCHIVE_STR,
276 Transport::ContainerStorage => Self::CONTAINERS_STORAGE_STR,
277 Transport::Dir => Self::LOCAL_DIRECTORY_STR,
278 Transport::DockerDaemon => Self::DOCKER_DAEMON_STR,
279 }
280 }
281}
282
283impl TryFrom<&str> for ImageReference {
284 type Error = anyhow::Error;
285
286 fn try_from(value: &str) -> Result<Self> {
287 let (transport_name, mut name) = value
288 .split_once(':')
289 .ok_or_else(|| anyhow!("Missing ':' in {}", value))?;
290 let transport: Transport = transport_name.try_into()?;
291 if name.is_empty() {
292 return Err(anyhow!("Invalid empty name in {}", value));
293 }
294 if transport_name == "docker" {
295 name = name
296 .strip_prefix("//")
297 .ok_or_else(|| anyhow!("Missing // in docker:// in {}", value))?;
298 }
299 Ok(Self {
300 transport,
301 name: name.to_string(),
302 })
303 }
304}
305
306impl FromStr for ImageReference {
307 type Err = anyhow::Error;
308
309 fn from_str(s: &str) -> Result<Self> {
310 Self::try_from(s)
311 }
312}
313
314impl TryFrom<&str> for SignatureSource {
315 type Error = anyhow::Error;
316
317 fn try_from(value: &str) -> Result<Self> {
318 match value {
319 "ostree-image-signed" => Ok(Self::ContainerPolicy),
320 "ostree-unverified-image" => Ok(Self::ContainerPolicyAllowInsecure),
321 o => match o.strip_prefix("ostree-remote-image:") {
322 Some(rest) => Ok(Self::OstreeRemote(rest.to_string())),
323 _ => Err(anyhow!("Invalid signature source: {}", o)),
324 },
325 }
326 }
327}
328
329impl FromStr for SignatureSource {
330 type Err = anyhow::Error;
331
332 fn from_str(s: &str) -> Result<Self> {
333 Self::try_from(s)
334 }
335}
336
337impl TryFrom<&str> for OstreeImageReference {
338 type Error = anyhow::Error;
339
340 fn try_from(value: &str) -> Result<Self> {
341 let (first, second) = value
342 .split_once(':')
343 .ok_or_else(|| anyhow!("Missing ':' in {}", value))?;
344 let (sigverify, rest) = match first {
345 "ostree-image-signed" => (SignatureSource::ContainerPolicy, Cow::Borrowed(second)),
346 "ostree-unverified-image" => (
347 SignatureSource::ContainerPolicyAllowInsecure,
348 Cow::Borrowed(second),
349 ),
350 "ostree-unverified-registry" => (
352 SignatureSource::ContainerPolicyAllowInsecure,
353 Cow::Owned(format!("registry:{second}")),
354 ),
355 "ostree-remote-registry" => {
357 let (remote, rest) = second
358 .split_once(':')
359 .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
360 (
361 SignatureSource::OstreeRemote(remote.to_string()),
362 Cow::Owned(format!("registry:{rest}")),
363 )
364 }
365 "ostree-remote-image" => {
366 let (remote, rest) = second
367 .split_once(':')
368 .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
369 (
370 SignatureSource::OstreeRemote(remote.to_string()),
371 Cow::Borrowed(rest),
372 )
373 }
374 o => {
375 return Err(anyhow!("Invalid ostree image reference scheme: {}", o));
376 }
377 };
378 let imgref = rest.deref().try_into()?;
379 Ok(Self { sigverify, imgref })
380 }
381}
382
383impl FromStr for OstreeImageReference {
384 type Err = anyhow::Error;
385
386 fn from_str(s: &str) -> Result<Self> {
387 Self::try_from(s)
388 }
389}
390
391impl std::fmt::Display for Transport {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 let s = match self {
394 Self::Registry => "docker://",
396 Self::OciArchive => "oci-archive:",
397 Self::DockerArchive => "docker-archive:",
398 Self::OciDir => "oci:",
399 Self::ContainerStorage => "containers-storage:",
400 Self::Dir => "dir:",
401 Self::DockerDaemon => "docker-daemon:",
402 };
403 f.write_str(s)
404 }
405}
406
407impl std::fmt::Display for ImageReference {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 write!(f, "{}{}", self.transport, self.name)
410 }
411}
412
413impl std::fmt::Display for SignatureSource {
414 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415 match self {
416 SignatureSource::OstreeRemote(r) => write!(f, "ostree-remote-image:{r}"),
417 SignatureSource::ContainerPolicy => write!(f, "ostree-image-signed"),
418 SignatureSource::ContainerPolicyAllowInsecure => {
419 write!(f, "ostree-unverified-image")
420 }
421 }
422 }
423}
424
425impl std::fmt::Display for OstreeImageReference {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 match (&self.sigverify, &self.imgref) {
428 (SignatureSource::ContainerPolicyAllowInsecure, imgref)
429 if imgref.transport == Transport::Registry =>
430 {
431 if f.alternate() {
435 write!(f, "{}", self.imgref)
436 } else {
437 write!(f, "ostree-unverified-registry:{}", self.imgref.name)
438 }
439 }
440 (sigverify, imgref) => {
441 write!(f, "{sigverify}:{imgref}")
442 }
443 }
444 }
445}
446
447#[derive(Debug, Serialize)]
449pub struct ManifestDiff<'a> {
450 #[serde(skip)]
452 pub from: &'a oci_spec::image::ImageManifest,
453 #[serde(skip)]
455 pub to: &'a oci_spec::image::ImageManifest,
456 #[serde(skip)]
458 pub removed: Vec<&'a oci_spec::image::Descriptor>,
459 #[serde(skip)]
461 pub added: Vec<&'a oci_spec::image::Descriptor>,
462 pub total: u64,
464 pub total_size: u64,
466 pub n_removed: u64,
468 pub removed_size: u64,
470 pub n_added: u64,
472 pub added_size: u64,
474}
475
476impl<'a> ManifestDiff<'a> {
477 pub fn new(
479 src: &'a oci_spec::image::ImageManifest,
480 dest: &'a oci_spec::image::ImageManifest,
481 ) -> Self {
482 let src_layers = src
483 .layers()
484 .iter()
485 .map(|l| (l.digest().digest(), l))
486 .collect::<HashMap<_, _>>();
487 let dest_layers = dest
488 .layers()
489 .iter()
490 .map(|l| (l.digest().digest(), l))
491 .collect::<HashMap<_, _>>();
492 let mut removed = Vec::new();
493 let mut added = Vec::new();
494 for (blobid, &descriptor) in src_layers.iter() {
495 if !dest_layers.contains_key(blobid) {
496 removed.push(descriptor);
497 }
498 }
499 removed.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
500 for (blobid, &descriptor) in dest_layers.iter() {
501 if !src_layers.contains_key(blobid) {
502 added.push(descriptor);
503 }
504 }
505 added.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
506
507 fn layersum<'a, I: Iterator<Item = &'a oci_spec::image::Descriptor>>(layers: I) -> u64 {
508 layers.map(|layer| layer.size()).sum()
509 }
510 let total = dest_layers.len() as u64;
511 let total_size = layersum(dest.layers().iter());
512 let n_removed = removed.len() as u64;
513 let n_added = added.len() as u64;
514 let removed_size = layersum(removed.iter().copied());
515 let added_size = layersum(added.iter().copied());
516 ManifestDiff {
517 from: src,
518 to: dest,
519 removed,
520 added,
521 total,
522 total_size,
523 n_removed,
524 removed_size,
525 n_added,
526 added_size,
527 }
528 }
529}
530
531impl ManifestDiff<'_> {
532 pub fn print(&self) {
534 let print_total = self.total;
535 let print_total_size = glib::format_size(self.total_size);
536 let print_n_removed = self.n_removed;
537 let print_removed_size = glib::format_size(self.removed_size);
538 let print_n_added = self.n_added;
539 let print_added_size = glib::format_size(self.added_size);
540 println!("Total new layers: {print_total:<4} Size: {print_total_size}");
541 println!("Removed layers: {print_n_removed:<4} Size: {print_removed_size}");
542 println!("Added layers: {print_n_added:<4} Size: {print_added_size}");
543 }
544}
545
546pub fn merge_default_container_proxy_opts(
552 config: &mut containers_image_proxy::ImageProxyConfig,
553) -> Result<()> {
554 let user = rustix::process::getuid()
555 .is_root()
556 .then_some(isolation::DEFAULT_UNPRIVILEGED_USER);
557 merge_default_container_proxy_opts_with_isolation(config, user)
558}
559
560pub fn merge_default_container_proxy_opts_with_isolation(
563 config: &mut containers_image_proxy::ImageProxyConfig,
564 isolation_user: Option<&str>,
565) -> Result<()> {
566 let auth_specified =
567 config.auth_anonymous || config.authfile.is_some() || config.auth_data.is_some();
568 if !auth_specified {
569 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
570 config.auth_data = crate::globals::get_global_authfile(root)?.map(|a| a.1);
571 if config.auth_data.is_none() {
575 config.auth_anonymous = true;
576 }
577 }
578 let isolation_user = config
581 .skopeo_cmd
582 .is_none()
583 .then_some(isolation_user.as_ref())
584 .flatten();
585 if let Some(user) = isolation_user {
586 if let Some(authfile) = config.authfile.take() {
589 config.auth_data = Some(std::fs::File::open(authfile)?);
590 }
591 let cmd = crate::isolation::unprivileged_subprocess("skopeo", user);
592 config.skopeo_cmd = Some(cmd);
593 }
594 Ok(())
595}
596
597pub(crate) fn labels_of(
599 config: &oci_spec::image::ImageConfiguration,
600) -> Option<&HashMap<String, String>> {
601 config.config().as_ref().and_then(|c| c.labels().as_ref())
602}
603
604pub fn version_for_config(config: &oci_spec::image::ImageConfiguration) -> Option<&str> {
606 if let Some(labels) = labels_of(config) {
607 for k in [oci_spec::image::ANNOTATION_VERSION, LABEL_VERSION] {
608 if let Some(v) = labels.get(k) {
609 return Some(v.as_str());
610 }
611 }
612 }
613 None
614}
615
616pub mod deploy;
617mod encapsulate;
618pub use encapsulate::*;
619mod unencapsulate;
620pub use unencapsulate::*;
621pub mod skopeo;
622pub mod store;
623mod update_detachedmeta;
624pub use update_detachedmeta::*;
625
626use crate::isolation;
627
628#[cfg(test)]
629mod tests {
630 use std::process::Command;
631
632 use containers_image_proxy::ImageProxyConfig;
633
634 use super::*;
635
636 #[test]
637 fn test_serializable_transport() {
638 for v in [
639 Transport::Registry,
640 Transport::ContainerStorage,
641 Transport::OciArchive,
642 Transport::DockerArchive,
643 Transport::OciDir,
644 ] {
645 assert_eq!(Transport::try_from(v.serializable_name()).unwrap(), v);
646 }
647 }
648
649 const INVALID_IRS: &[&str] = &["", "foo://", "docker:blah", "registry:", "foo:bar"];
650 const VALID_IRS: &[&str] = &[
651 "containers-storage:localhost/someimage",
652 "docker://quay.io/exampleos/blah:sometag",
653 ];
654
655 #[test]
656 fn test_imagereference() {
657 let ir: ImageReference = "registry:quay.io/exampleos/blah".try_into().unwrap();
658 assert_eq!(ir.transport, Transport::Registry);
659 assert_eq!(ir.name, "quay.io/exampleos/blah");
660 assert_eq!(ir.to_string(), "docker://quay.io/exampleos/blah");
661
662 for &v in VALID_IRS {
663 ImageReference::try_from(v).unwrap();
664 }
665
666 for &v in INVALID_IRS {
667 if ImageReference::try_from(v).is_ok() {
668 panic!("Should fail to parse: {v}")
669 }
670 }
671 struct Case {
672 s: &'static str,
673 transport: Transport,
674 name: &'static str,
675 }
676 for case in [
677 Case {
678 s: "oci:somedir",
679 transport: Transport::OciDir,
680 name: "somedir",
681 },
682 Case {
683 s: "dir:/some/dir/blah",
684 transport: Transport::Dir,
685 name: "/some/dir/blah",
686 },
687 Case {
688 s: "oci-archive:/path/to/foo.ociarchive",
689 transport: Transport::OciArchive,
690 name: "/path/to/foo.ociarchive",
691 },
692 Case {
693 s: "docker-archive:/path/to/foo.dockerarchive",
694 transport: Transport::DockerArchive,
695 name: "/path/to/foo.dockerarchive",
696 },
697 Case {
698 s: "containers-storage:localhost/someimage:blah",
699 transport: Transport::ContainerStorage,
700 name: "localhost/someimage:blah",
701 },
702 ] {
703 let ir: ImageReference = case.s.try_into().unwrap();
704 assert_eq!(ir.transport, case.transport);
705 assert_eq!(ir.name, case.name);
706 let reserialized = ir.to_string();
707 assert_eq!(case.s, reserialized.as_str());
708 }
709 }
710
711 #[test]
712 fn test_ostreeimagereference() {
713 let ir_s = "ostree-remote-image:myremote:registry:quay.io/exampleos/blah";
716 let ir_registry = "ostree-remote-registry:myremote:quay.io/exampleos/blah";
717 for &ir_s in &[ir_s, ir_registry] {
718 let ir: OstreeImageReference = ir_s.try_into().unwrap();
719 assert_eq!(
720 ir.sigverify,
721 SignatureSource::OstreeRemote("myremote".to_string())
722 );
723 assert_eq!(ir.imgref.transport, Transport::Registry);
724 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
725 assert_eq!(
726 ir.to_string(),
727 "ostree-remote-image:myremote:docker://quay.io/exampleos/blah"
728 );
729 }
730
731 let ir: OstreeImageReference = ir_s.try_into().unwrap();
734 assert_eq!(ir, OstreeImageReference::from_str(ir_s).unwrap());
735 assert_eq!(&ir, &OstreeImageReference::try_from(ir_registry).unwrap());
737
738 let ir_s = "ostree-image-signed:docker://quay.io/exampleos/blah";
739 let ir: OstreeImageReference = ir_s.try_into().unwrap();
740 assert_eq!(ir.sigverify, SignatureSource::ContainerPolicy);
741 assert_eq!(ir.imgref.transport, Transport::Registry);
742 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
743 assert_eq!(ir.to_string(), ir_s);
744 assert_eq!(format!("{:#}", &ir), ir_s);
745
746 let ir_s = "ostree-unverified-image:docker://quay.io/exampleos/blah";
747 let ir: OstreeImageReference = ir_s.try_into().unwrap();
748 assert_eq!(ir.sigverify, SignatureSource::ContainerPolicyAllowInsecure);
749 assert_eq!(ir.imgref.transport, Transport::Registry);
750 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
751 assert_eq!(
752 ir.to_string(),
753 "ostree-unverified-registry:quay.io/exampleos/blah"
754 );
755 let ir_shorthand =
756 OstreeImageReference::try_from("ostree-unverified-registry:quay.io/exampleos/blah")
757 .unwrap();
758 assert_eq!(&ir_shorthand, &ir);
759 assert_eq!(format!("{:#}", &ir), "docker://quay.io/exampleos/blah");
760 }
761
762 #[test]
763 fn test_merge_authopts() {
764 let mut c = ImageProxyConfig::default();
766 let authf = std::fs::File::open("/dev/null").unwrap();
767 c.auth_data = Some(authf);
768 super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
769 assert!(!c.auth_anonymous);
770 assert!(c.authfile.is_none());
771 assert!(c.auth_data.is_some());
772 assert!(c.skopeo_cmd.is_none());
773 super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
774 assert!(!c.auth_anonymous);
775 assert!(c.authfile.is_none());
776 assert!(c.auth_data.is_some());
777 assert!(c.skopeo_cmd.is_none());
778
779 let mut c = ImageProxyConfig {
781 skopeo_cmd: Some(Command::new("skopeo")),
782 ..Default::default()
783 };
784 super::merge_default_container_proxy_opts_with_isolation(&mut c, Some("foo")).unwrap();
785 assert_eq!(c.skopeo_cmd.unwrap().get_program(), "skopeo");
786 }
787}