1use anyhow::anyhow;
29use cap_std_ext::cap_std;
30use cap_std_ext::cap_std::fs::Dir;
31use containers_image_proxy::oci_spec;
32use ostree::glib;
33use serde::Serialize;
34
35use std::borrow::Cow;
36use std::collections::HashMap;
37use std::fmt::Debug;
38use std::ops::Deref;
39use std::str::FromStr;
40
41pub const OSTREE_COMMIT_LABEL: &str = "ostree.commit";
43
44pub(crate) const CONTENT_ANNOTATION: &str = "ostree.components";
47pub(crate) const COMPONENT_SEPARATOR: char = ',';
49
50type Result<T> = anyhow::Result<T>;
53
54#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq)]
56pub enum Transport {
57 Registry,
59 OciDir,
61 OciArchive,
63 DockerArchive,
65 ContainerStorage,
67 Dir,
69}
70
71#[derive(Debug, Clone, Hash, PartialEq, Eq)]
75pub struct ImageReference {
76 pub transport: Transport,
78 pub name: String,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Hash)]
84pub enum SignatureSource {
85 OstreeRemote(String),
87 ContainerPolicy,
89 ContainerPolicyAllowInsecure,
91}
92
93pub const LABEL_VERSION: &str = "version";
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99pub struct OstreeImageReference {
100 pub sigverify: SignatureSource,
102 pub imgref: ImageReference,
104}
105
106impl TryFrom<&str> for Transport {
107 type Error = anyhow::Error;
108
109 fn try_from(value: &str) -> Result<Self> {
110 Ok(match value {
111 Self::REGISTRY_STR | "docker" => Self::Registry,
112 Self::OCI_STR => Self::OciDir,
113 Self::OCI_ARCHIVE_STR => Self::OciArchive,
114 Self::DOCKER_ARCHIVE_STR => Self::DockerArchive,
115 Self::CONTAINERS_STORAGE_STR => Self::ContainerStorage,
116 Self::LOCAL_DIRECTORY_STR => Self::Dir,
117 o => return Err(anyhow!("Unknown transport '{}'", o)),
118 })
119 }
120}
121
122impl Transport {
123 const OCI_STR: &'static str = "oci";
124 const OCI_ARCHIVE_STR: &'static str = "oci-archive";
125 const DOCKER_ARCHIVE_STR: &'static str = "docker-archive";
126 const CONTAINERS_STORAGE_STR: &'static str = "containers-storage";
127 const LOCAL_DIRECTORY_STR: &'static str = "dir";
128 const REGISTRY_STR: &'static str = "registry";
129
130 pub fn serializable_name(&self) -> &'static str {
132 match self {
133 Transport::Registry => Self::REGISTRY_STR,
134 Transport::OciDir => Self::OCI_STR,
135 Transport::OciArchive => Self::OCI_ARCHIVE_STR,
136 Transport::DockerArchive => Self::DOCKER_ARCHIVE_STR,
137 Transport::ContainerStorage => Self::CONTAINERS_STORAGE_STR,
138 Transport::Dir => Self::LOCAL_DIRECTORY_STR,
139 }
140 }
141}
142
143impl TryFrom<&str> for ImageReference {
144 type Error = anyhow::Error;
145
146 fn try_from(value: &str) -> Result<Self> {
147 let (transport_name, mut name) = value
148 .split_once(':')
149 .ok_or_else(|| anyhow!("Missing ':' in {}", value))?;
150 let transport: Transport = transport_name.try_into()?;
151 if name.is_empty() {
152 return Err(anyhow!("Invalid empty name in {}", value));
153 }
154 if transport_name == "docker" {
155 name = name
156 .strip_prefix("//")
157 .ok_or_else(|| anyhow!("Missing // in docker:// in {}", value))?;
158 }
159 Ok(Self {
160 transport,
161 name: name.to_string(),
162 })
163 }
164}
165
166impl FromStr for ImageReference {
167 type Err = anyhow::Error;
168
169 fn from_str(s: &str) -> Result<Self> {
170 Self::try_from(s)
171 }
172}
173
174impl TryFrom<&str> for SignatureSource {
175 type Error = anyhow::Error;
176
177 fn try_from(value: &str) -> Result<Self> {
178 match value {
179 "ostree-image-signed" => Ok(Self::ContainerPolicy),
180 "ostree-unverified-image" => Ok(Self::ContainerPolicyAllowInsecure),
181 o => match o.strip_prefix("ostree-remote-image:") {
182 Some(rest) => Ok(Self::OstreeRemote(rest.to_string())),
183 _ => Err(anyhow!("Invalid signature source: {}", o)),
184 },
185 }
186 }
187}
188
189impl FromStr for SignatureSource {
190 type Err = anyhow::Error;
191
192 fn from_str(s: &str) -> Result<Self> {
193 Self::try_from(s)
194 }
195}
196
197impl TryFrom<&str> for OstreeImageReference {
198 type Error = anyhow::Error;
199
200 fn try_from(value: &str) -> Result<Self> {
201 let (first, second) = value
202 .split_once(':')
203 .ok_or_else(|| anyhow!("Missing ':' in {}", value))?;
204 let (sigverify, rest) = match first {
205 "ostree-image-signed" => (SignatureSource::ContainerPolicy, Cow::Borrowed(second)),
206 "ostree-unverified-image" => (
207 SignatureSource::ContainerPolicyAllowInsecure,
208 Cow::Borrowed(second),
209 ),
210 "ostree-unverified-registry" => (
212 SignatureSource::ContainerPolicyAllowInsecure,
213 Cow::Owned(format!("registry:{second}")),
214 ),
215 "ostree-remote-registry" => {
217 let (remote, rest) = second
218 .split_once(':')
219 .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
220 (
221 SignatureSource::OstreeRemote(remote.to_string()),
222 Cow::Owned(format!("registry:{rest}")),
223 )
224 }
225 "ostree-remote-image" => {
226 let (remote, rest) = second
227 .split_once(':')
228 .ok_or_else(|| anyhow!("Missing second ':' in {}", value))?;
229 (
230 SignatureSource::OstreeRemote(remote.to_string()),
231 Cow::Borrowed(rest),
232 )
233 }
234 o => {
235 return Err(anyhow!("Invalid ostree image reference scheme: {}", o));
236 }
237 };
238 let imgref = rest.deref().try_into()?;
239 Ok(Self { sigverify, imgref })
240 }
241}
242
243impl FromStr for OstreeImageReference {
244 type Err = anyhow::Error;
245
246 fn from_str(s: &str) -> Result<Self> {
247 Self::try_from(s)
248 }
249}
250
251impl std::fmt::Display for Transport {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 let s = match self {
254 Self::Registry => "docker://",
256 Self::OciArchive => "oci-archive:",
257 Self::DockerArchive => "docker-archive:",
258 Self::OciDir => "oci:",
259 Self::ContainerStorage => "containers-storage:",
260 Self::Dir => "dir:",
261 };
262 f.write_str(s)
263 }
264}
265
266impl std::fmt::Display for ImageReference {
267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268 write!(f, "{}{}", self.transport, self.name)
269 }
270}
271
272impl std::fmt::Display for SignatureSource {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 match self {
275 SignatureSource::OstreeRemote(r) => write!(f, "ostree-remote-image:{r}"),
276 SignatureSource::ContainerPolicy => write!(f, "ostree-image-signed"),
277 SignatureSource::ContainerPolicyAllowInsecure => {
278 write!(f, "ostree-unverified-image")
279 }
280 }
281 }
282}
283
284impl std::fmt::Display for OstreeImageReference {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 match (&self.sigverify, &self.imgref) {
287 (SignatureSource::ContainerPolicyAllowInsecure, imgref)
288 if imgref.transport == Transport::Registry =>
289 {
290 if f.alternate() {
294 write!(f, "{}", self.imgref)
295 } else {
296 write!(f, "ostree-unverified-registry:{}", self.imgref.name)
297 }
298 }
299 (sigverify, imgref) => {
300 write!(f, "{}:{}", sigverify, imgref)
301 }
302 }
303 }
304}
305
306#[derive(Debug, Serialize)]
308pub struct ManifestDiff<'a> {
309 #[serde(skip)]
311 pub from: &'a oci_spec::image::ImageManifest,
312 #[serde(skip)]
314 pub to: &'a oci_spec::image::ImageManifest,
315 #[serde(skip)]
317 pub removed: Vec<&'a oci_spec::image::Descriptor>,
318 #[serde(skip)]
320 pub added: Vec<&'a oci_spec::image::Descriptor>,
321 pub total: u64,
323 pub total_size: u64,
325 pub n_removed: u64,
327 pub removed_size: u64,
329 pub n_added: u64,
331 pub added_size: u64,
333}
334
335impl<'a> ManifestDiff<'a> {
336 pub fn new(
338 src: &'a oci_spec::image::ImageManifest,
339 dest: &'a oci_spec::image::ImageManifest,
340 ) -> Self {
341 let src_layers = src
342 .layers()
343 .iter()
344 .map(|l| (l.digest().digest(), l))
345 .collect::<HashMap<_, _>>();
346 let dest_layers = dest
347 .layers()
348 .iter()
349 .map(|l| (l.digest().digest(), l))
350 .collect::<HashMap<_, _>>();
351 let mut removed = Vec::new();
352 let mut added = Vec::new();
353 for (blobid, &descriptor) in src_layers.iter() {
354 if !dest_layers.contains_key(blobid) {
355 removed.push(descriptor);
356 }
357 }
358 removed.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
359 for (blobid, &descriptor) in dest_layers.iter() {
360 if !src_layers.contains_key(blobid) {
361 added.push(descriptor);
362 }
363 }
364 added.sort_by(|a, b| a.digest().digest().cmp(b.digest().digest()));
365
366 fn layersum<'a, I: Iterator<Item = &'a oci_spec::image::Descriptor>>(layers: I) -> u64 {
367 layers.map(|layer| layer.size()).sum()
368 }
369 let total = dest_layers.len() as u64;
370 let total_size = layersum(dest.layers().iter());
371 let n_removed = removed.len() as u64;
372 let n_added = added.len() as u64;
373 let removed_size = layersum(removed.iter().copied());
374 let added_size = layersum(added.iter().copied());
375 ManifestDiff {
376 from: src,
377 to: dest,
378 removed,
379 added,
380 total,
381 total_size,
382 n_removed,
383 removed_size,
384 n_added,
385 added_size,
386 }
387 }
388}
389
390impl<'a> ManifestDiff<'a> {
391 pub fn print(&self) {
393 let print_total = self.total;
394 let print_total_size = glib::format_size(self.total_size);
395 let print_n_removed = self.n_removed;
396 let print_removed_size = glib::format_size(self.removed_size);
397 let print_n_added = self.n_added;
398 let print_added_size = glib::format_size(self.added_size);
399 println!("Total new layers: {print_total:<4} Size: {print_total_size}");
400 println!("Removed layers: {print_n_removed:<4} Size: {print_removed_size}");
401 println!("Added layers: {print_n_added:<4} Size: {print_added_size}");
402 }
403}
404
405pub fn merge_default_container_proxy_opts(
411 config: &mut containers_image_proxy::ImageProxyConfig,
412) -> Result<()> {
413 let user = rustix::process::getuid()
414 .is_root()
415 .then_some(isolation::DEFAULT_UNPRIVILEGED_USER);
416 merge_default_container_proxy_opts_with_isolation(config, user)
417}
418
419pub fn merge_default_container_proxy_opts_with_isolation(
422 config: &mut containers_image_proxy::ImageProxyConfig,
423 isolation_user: Option<&str>,
424) -> Result<()> {
425 let auth_specified =
426 config.auth_anonymous || config.authfile.is_some() || config.auth_data.is_some();
427 if !auth_specified {
428 let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
429 config.auth_data = crate::globals::get_global_authfile(root)?.map(|a| a.1);
430 if config.auth_data.is_none() {
434 config.auth_anonymous = true;
435 }
436 }
437 let isolation_user = config
440 .skopeo_cmd
441 .is_none()
442 .then_some(isolation_user.as_ref())
443 .flatten();
444 if let Some(user) = isolation_user {
445 if let Some(authfile) = config.authfile.take() {
448 config.auth_data = Some(std::fs::File::open(authfile)?);
449 }
450 let cmd = crate::isolation::unprivileged_subprocess("skopeo", user);
451 config.skopeo_cmd = Some(cmd);
452 }
453 Ok(())
454}
455
456pub(crate) fn labels_of(
458 config: &oci_spec::image::ImageConfiguration,
459) -> Option<&HashMap<String, String>> {
460 config.config().as_ref().and_then(|c| c.labels().as_ref())
461}
462
463pub fn version_for_config(config: &oci_spec::image::ImageConfiguration) -> Option<&str> {
465 if let Some(labels) = labels_of(config) {
466 for k in [oci_spec::image::ANNOTATION_VERSION, LABEL_VERSION] {
467 if let Some(v) = labels.get(k) {
468 return Some(v.as_str());
469 }
470 }
471 }
472 None
473}
474
475pub mod deploy;
476mod encapsulate;
477pub use encapsulate::*;
478mod unencapsulate;
479pub use unencapsulate::*;
480mod skopeo;
481pub mod store;
482mod update_detachedmeta;
483pub use update_detachedmeta::*;
484
485use crate::isolation;
486
487#[cfg(test)]
488mod tests {
489 use std::process::Command;
490
491 use containers_image_proxy::ImageProxyConfig;
492
493 use super::*;
494
495 #[test]
496 fn test_serializable_transport() {
497 for v in [
498 Transport::Registry,
499 Transport::ContainerStorage,
500 Transport::OciArchive,
501 Transport::DockerArchive,
502 Transport::OciDir,
503 ] {
504 assert_eq!(Transport::try_from(v.serializable_name()).unwrap(), v);
505 }
506 }
507
508 const INVALID_IRS: &[&str] = &["", "foo://", "docker:blah", "registry:", "foo:bar"];
509 const VALID_IRS: &[&str] = &[
510 "containers-storage:localhost/someimage",
511 "docker://quay.io/exampleos/blah:sometag",
512 ];
513
514 #[test]
515 fn test_imagereference() {
516 let ir: ImageReference = "registry:quay.io/exampleos/blah".try_into().unwrap();
517 assert_eq!(ir.transport, Transport::Registry);
518 assert_eq!(ir.name, "quay.io/exampleos/blah");
519 assert_eq!(ir.to_string(), "docker://quay.io/exampleos/blah");
520
521 for &v in VALID_IRS {
522 ImageReference::try_from(v).unwrap();
523 }
524
525 for &v in INVALID_IRS {
526 if ImageReference::try_from(v).is_ok() {
527 panic!("Should fail to parse: {}", v)
528 }
529 }
530 struct Case {
531 s: &'static str,
532 transport: Transport,
533 name: &'static str,
534 }
535 for case in [
536 Case {
537 s: "oci:somedir",
538 transport: Transport::OciDir,
539 name: "somedir",
540 },
541 Case {
542 s: "dir:/some/dir/blah",
543 transport: Transport::Dir,
544 name: "/some/dir/blah",
545 },
546 Case {
547 s: "oci-archive:/path/to/foo.ociarchive",
548 transport: Transport::OciArchive,
549 name: "/path/to/foo.ociarchive",
550 },
551 Case {
552 s: "docker-archive:/path/to/foo.dockerarchive",
553 transport: Transport::DockerArchive,
554 name: "/path/to/foo.dockerarchive",
555 },
556 Case {
557 s: "containers-storage:localhost/someimage:blah",
558 transport: Transport::ContainerStorage,
559 name: "localhost/someimage:blah",
560 },
561 ] {
562 let ir: ImageReference = case.s.try_into().unwrap();
563 assert_eq!(ir.transport, case.transport);
564 assert_eq!(ir.name, case.name);
565 let reserialized = ir.to_string();
566 assert_eq!(case.s, reserialized.as_str());
567 }
568 }
569
570 #[test]
571 fn test_ostreeimagereference() {
572 let ir_s = "ostree-remote-image:myremote:registry:quay.io/exampleos/blah";
575 let ir_registry = "ostree-remote-registry:myremote:quay.io/exampleos/blah";
576 for &ir_s in &[ir_s, ir_registry] {
577 let ir: OstreeImageReference = ir_s.try_into().unwrap();
578 assert_eq!(
579 ir.sigverify,
580 SignatureSource::OstreeRemote("myremote".to_string())
581 );
582 assert_eq!(ir.imgref.transport, Transport::Registry);
583 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
584 assert_eq!(
585 ir.to_string(),
586 "ostree-remote-image:myremote:docker://quay.io/exampleos/blah"
587 );
588 }
589
590 let ir: OstreeImageReference = ir_s.try_into().unwrap();
593 assert_eq!(ir, OstreeImageReference::from_str(ir_s).unwrap());
594 assert_eq!(&ir, &OstreeImageReference::try_from(ir_registry).unwrap());
596
597 let ir_s = "ostree-image-signed:docker://quay.io/exampleos/blah";
598 let ir: OstreeImageReference = ir_s.try_into().unwrap();
599 assert_eq!(ir.sigverify, SignatureSource::ContainerPolicy);
600 assert_eq!(ir.imgref.transport, Transport::Registry);
601 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
602 assert_eq!(ir.to_string(), ir_s);
603 assert_eq!(format!("{:#}", &ir), ir_s);
604
605 let ir_s = "ostree-unverified-image:docker://quay.io/exampleos/blah";
606 let ir: OstreeImageReference = ir_s.try_into().unwrap();
607 assert_eq!(ir.sigverify, SignatureSource::ContainerPolicyAllowInsecure);
608 assert_eq!(ir.imgref.transport, Transport::Registry);
609 assert_eq!(ir.imgref.name, "quay.io/exampleos/blah");
610 assert_eq!(
611 ir.to_string(),
612 "ostree-unverified-registry:quay.io/exampleos/blah"
613 );
614 let ir_shorthand =
615 OstreeImageReference::try_from("ostree-unverified-registry:quay.io/exampleos/blah")
616 .unwrap();
617 assert_eq!(&ir_shorthand, &ir);
618 assert_eq!(format!("{:#}", &ir), "docker://quay.io/exampleos/blah");
619 }
620
621 #[test]
622 fn test_merge_authopts() {
623 let mut c = ImageProxyConfig::default();
625 let authf = std::fs::File::open("/dev/null").unwrap();
626 c.auth_data = Some(authf);
627 super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
628 assert!(!c.auth_anonymous);
629 assert!(c.authfile.is_none());
630 assert!(c.auth_data.is_some());
631 assert!(c.skopeo_cmd.is_none());
632 super::merge_default_container_proxy_opts_with_isolation(&mut c, None).unwrap();
633 assert!(!c.auth_anonymous);
634 assert!(c.authfile.is_none());
635 assert!(c.auth_data.is_some());
636 assert!(c.skopeo_cmd.is_none());
637
638 let mut c = ImageProxyConfig {
640 skopeo_cmd: Some(Command::new("skopeo")),
641 ..Default::default()
642 };
643 super::merge_default_container_proxy_opts_with_isolation(&mut c, Some("foo")).unwrap();
644 assert_eq!(c.skopeo_cmd.unwrap().get_program(), "skopeo");
645 }
646}