krataoci/
name.rs

1use anyhow::Result;
2use std::fmt;
3use url::Url;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct ImageName {
7    pub hostname: String,
8    pub port: Option<u16>,
9    pub name: String,
10    pub reference: Option<String>,
11    pub digest: Option<String>,
12}
13
14impl fmt::Display for ImageName {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        let mut suffix = String::new();
17
18        if let Some(ref reference) = self.reference {
19            suffix.push(':');
20            suffix.push_str(reference);
21        }
22
23        if let Some(ref digest) = self.digest {
24            suffix.push('@');
25            suffix.push_str(digest);
26        }
27
28        if ImageName::DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() {
29            if self.name.starts_with("library/") {
30                write!(f, "{}{}", &self.name[8..], suffix)
31            } else {
32                write!(f, "{}{}", self.name, suffix)
33            }
34        } else if let Some(port) = self.port {
35            write!(f, "{}:{}/{}{}", self.hostname, port, self.name, suffix)
36        } else {
37            write!(f, "{}/{}{}", self.hostname, self.name, suffix)
38        }
39    }
40}
41
42impl Default for ImageName {
43    fn default() -> Self {
44        Self::parse(&format!("{}", uuid::Uuid::new_v4().as_hyphenated()))
45            .expect("UUID hyphenated must be valid name")
46    }
47}
48
49impl ImageName {
50    pub const DOCKER_HUB_MIRROR: &'static str = "mirror.gcr.io";
51    pub const DEFAULT_IMAGE_TAG: &'static str = "latest";
52
53    pub fn parse(name: &str) -> Result<Self> {
54        let full_name = name.to_string();
55        let name = full_name.clone();
56        let (mut hostname, mut name) = name
57            .split_once('/')
58            .map(|x| (x.0.to_string(), x.1.to_string()))
59            .unwrap_or_else(|| {
60                (
61                    ImageName::DOCKER_HUB_MIRROR.to_string(),
62                    format!("library/{}", name),
63                )
64            });
65
66        // heuristic to find any docker hub image formats
67        // that may be in the hostname format. for example:
68        // abc/xyz:latest will trigger this if check, but abc.io/xyz:latest will not,
69        // and neither will abc/hello/xyz:latest
70        if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 {
71            name = format!("{}/{}", hostname, name);
72            hostname = ImageName::DOCKER_HUB_MIRROR.to_string();
73        }
74
75        let (hostname, port) = if let Some((hostname, port)) = hostname
76            .split_once(':')
77            .map(|x| (x.0.to_string(), x.1.to_string()))
78        {
79            (hostname, Some(str::parse(&port)?))
80        } else {
81            (hostname, None)
82        };
83
84        let name_has_digest = if name.contains('@') {
85            let digest_start = name.chars().position(|c| c == '@');
86            let ref_start = name.chars().position(|c| c == ':');
87            if let (Some(digest_start), Some(ref_start)) = (digest_start, ref_start) {
88                digest_start < ref_start
89            } else {
90                true
91            }
92        } else {
93            false
94        };
95
96        let (name, digest) = if name_has_digest {
97            name.split_once('@')
98                .map(|(name, digest)| (name.to_string(), Some(digest.to_string())))
99                .unwrap_or_else(|| (name, None))
100        } else {
101            (name, None)
102        };
103
104        let (name, reference) = if name.contains(':') {
105            name.split_once(':')
106                .map(|(name, reference)| (name.to_string(), Some(reference.to_string())))
107                .unwrap_or((name, None))
108        } else {
109            (name, None)
110        };
111
112        let (reference, digest) = if let Some(reference) = reference {
113            if let Some(digest) = digest {
114                (Some(reference), Some(digest))
115            } else {
116                reference
117                    .split_once('@')
118                    .map(|(reff, digest)| (Some(reff.to_string()), Some(digest.to_string())))
119                    .unwrap_or_else(|| (Some(reference), None))
120            }
121        } else {
122            (None, digest)
123        };
124
125        Ok(ImageName {
126            hostname,
127            port,
128            name,
129            reference,
130            digest,
131        })
132    }
133
134    pub fn registry_url(&self) -> Result<Url> {
135        let hostname = if let Some(port) = self.port {
136            format!("{}:{}", self.hostname, port)
137        } else {
138            self.hostname.clone()
139        };
140        let url = if self.hostname.starts_with("localhost") {
141            format!("http://{}", hostname)
142        } else {
143            format!("https://{}", hostname)
144        };
145        Ok(Url::parse(&url)?)
146    }
147}