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