Skip to main content

blue_build_utils/
container.rs

1use std::{
2    borrow::Cow,
3    ffi::OsString,
4    ops::Deref,
5    path::{Path, PathBuf},
6    str::FromStr,
7};
8
9use lazy_regex::regex;
10use miette::miette;
11use oci_client::Reference;
12use serde::{Deserialize, Serialize};
13
14use crate::platform::Platform;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ContainerId(pub String);
18
19impl Deref for ContainerId {
20    type Target = str;
21
22    fn deref(&self) -> &Self::Target {
23        &self.0
24    }
25}
26
27impl std::fmt::Display for ContainerId {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{}", &self.0)
30    }
31}
32
33impl AsRef<std::ffi::OsStr> for ContainerId {
34    fn as_ref(&self) -> &std::ffi::OsStr {
35        self.0.as_ref()
36    }
37}
38
39pub struct MountId(pub String);
40
41impl Deref for MountId {
42    type Target = str;
43
44    fn deref(&self) -> &Self::Target {
45        &self.0
46    }
47}
48
49impl std::fmt::Display for MountId {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}", &self.0)
52    }
53}
54
55impl AsRef<std::ffi::OsStr> for MountId {
56    fn as_ref(&self) -> &std::ffi::OsStr {
57        self.0.as_ref()
58    }
59}
60
61impl<'a> From<&'a MountId> for std::borrow::Cow<'a, str> {
62    fn from(value: &'a MountId) -> Self {
63        Self::Borrowed(&value.0)
64    }
65}
66
67#[derive(Clone, Debug)]
68pub enum OciRef {
69    LocalStorage(Reference),
70    OciArchive(PathBuf),
71    OciDir(PathBuf),
72    Remote(Reference),
73}
74
75impl std::fmt::Display for OciRef {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Self::LocalStorage(local_ref) => write!(f, "containers-storage:{}", local_ref.whole()),
79            Self::OciArchive(path) => write!(f, "oci-archive:{}", path.display()),
80            Self::OciDir(path) => write!(f, "oci:{}", path.display()),
81            Self::Remote(image_ref) => write!(f, "docker://{}", image_ref.whole()),
82        }
83    }
84}
85
86impl OciRef {
87    #[must_use]
88    pub fn from_local_storage(local_ref: &Reference) -> Self {
89        Self::LocalStorage(local_ref.clone())
90    }
91
92    /// # Errors
93    /// Returns an error if the path does not point to a regular file.
94    pub fn from_oci_archive<P: AsRef<Path>>(path: P) -> Result<Self, miette::Report> {
95        if !path.as_ref().is_file() {
96            miette::bail!("OCI archive doesn't exist at {}", path.as_ref().display());
97        }
98
99        Ok(Self::OciArchive(path.as_ref().to_owned()))
100    }
101
102    /// # Errors
103    /// Returns an error if the path does not point to a directory.
104    pub fn from_oci_directory<P: AsRef<Path>>(path: P) -> Result<Self, miette::Report> {
105        if !path.as_ref().is_dir() {
106            miette::bail!("OCI directory doesn't exist at {}", path.as_ref().display());
107        }
108
109        Ok(Self::OciDir(path.as_ref().to_owned()))
110    }
111
112    #[must_use]
113    pub fn from_remote_ref(image_ref: &Reference) -> Self {
114        Self::Remote(image_ref.clone())
115    }
116
117    #[must_use]
118    pub fn to_os_string(&self) -> OsString {
119        match self {
120            Self::LocalStorage(local_ref) => format!("containers-storage:{local_ref}").into(),
121            Self::OciArchive(path) => {
122                let mut out = OsString::from("oci-archive:");
123                out.push(path.as_os_str());
124                out
125            }
126            Self::OciDir(path) => {
127                let mut out = OsString::from("oci:");
128                out.push(path.as_os_str());
129                out
130            }
131            Self::Remote(image_ref) => format!("docker://{}", image_ref.whole()).into(),
132        }
133    }
134}
135
136/// An image ref that could reference
137/// a remote registry or a local tarball.
138#[derive(Debug, Clone)]
139pub enum ImageRef<'scope> {
140    Remote(Cow<'scope, Reference>),
141    LocalTar(Cow<'scope, Path>),
142    Other(Cow<'scope, str>),
143}
144
145impl<'scope> ImageRef<'scope> {
146    #[must_use]
147    pub fn remote_ref(&self) -> Option<&Reference> {
148        match self {
149            Self::Remote(remote) => Some(remote.as_ref()),
150            _ => None,
151        }
152    }
153
154    #[must_use]
155    pub fn with_platform(&'scope self, platform: Platform) -> Self {
156        if let Self::Remote(remote) = &self {
157            Self::Remote(Cow::Owned(platform.tagged_image(remote)))
158        } else if let Self::LocalTar(path) = &self
159            && let Some(tagged) = platform.tagged_path(path)
160        {
161            Self::LocalTar(Cow::Owned(tagged))
162        } else {
163            Self::from(self)
164        }
165    }
166
167    /// Appends a value to the end of a tag.
168    ///
169    /// If the ref is a tarball, it will append it to the file
170    /// stem. If it's other, it will append to the end of the value.
171    #[must_use]
172    pub fn append_tag(&self, value: &Tag) -> Self {
173        match self {
174            Self::Remote(image) => Self::Remote(Cow::Owned(Reference::with_tag(
175                image.registry().to_owned(),
176                image.repository().to_owned(),
177                image
178                    .tag()
179                    .map_or_else(|| format!("latest_{value}"), |tag| format!("{tag}_{value}")),
180            ))),
181            Self::LocalTar(path) => {
182                if let Some(file_stem) = path.file_stem()
183                    && let Some(extension) = path.extension()
184                {
185                    Self::LocalTar(Cow::Owned(
186                        path.with_file_name(format!("{}_{value}", file_stem.display(),))
187                            .with_extension(extension),
188                    ))
189                } else {
190                    Self::LocalTar(Cow::Owned(PathBuf::from(format!(
191                        "{}_{value}",
192                        path.display()
193                    ))))
194                }
195            }
196            Self::Other(other) => Self::Other(Cow::Owned(format!("{other}_{value}"))),
197        }
198    }
199}
200
201impl<'scope> From<&'scope Self> for ImageRef<'scope> {
202    fn from(value: &'scope ImageRef) -> Self {
203        match value {
204            Self::Remote(remote) => Self::Remote(Cow::Borrowed(remote.as_ref())),
205            Self::LocalTar(path) => Self::LocalTar(Cow::Borrowed(path.as_ref())),
206            Self::Other(other) => Self::Other(Cow::Borrowed(other.as_ref())),
207        }
208    }
209}
210
211impl<'scope> From<&'scope Reference> for ImageRef<'scope> {
212    fn from(value: &'scope Reference) -> Self {
213        Self::Remote(Cow::Borrowed(value))
214    }
215}
216
217impl From<Reference> for ImageRef<'_> {
218    fn from(value: Reference) -> Self {
219        Self::Remote(Cow::Owned(value))
220    }
221}
222
223impl<'scope> From<&'scope Path> for ImageRef<'scope> {
224    fn from(value: &'scope Path) -> Self {
225        Self::LocalTar(Cow::Borrowed(value))
226    }
227}
228
229impl<'scope> From<&'scope PathBuf> for ImageRef<'scope> {
230    fn from(value: &'scope PathBuf) -> Self {
231        Self::from(value.as_path())
232    }
233}
234
235impl From<PathBuf> for ImageRef<'_> {
236    fn from(value: PathBuf) -> Self {
237        Self::LocalTar(Cow::Owned(value))
238    }
239}
240
241impl From<ImageRef<'_>> for String {
242    fn from(value: ImageRef<'_>) -> Self {
243        Self::from(&value)
244    }
245}
246
247impl From<&ImageRef<'_>> for String {
248    fn from(value: &ImageRef<'_>) -> Self {
249        format!("{value}")
250    }
251}
252
253impl std::fmt::Display for ImageRef<'_> {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        write!(
256            f,
257            "{}",
258            match self {
259                Self::Remote(remote) => remote.whole(),
260                Self::LocalTar(path) => format!("oci-archive:{}", path.display()),
261                Self::Other(other) => other.to_string(),
262            }
263        )
264    }
265}
266
267impl PartialEq<Reference> for ImageRef<'_> {
268    fn eq(&self, other: &Reference) -> bool {
269        match self {
270            Self::Remote(remote) => &**remote == other,
271            _ => false,
272        }
273    }
274}
275
276#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
277pub struct Tag(String);
278
279impl Tag {
280    #[must_use]
281    pub fn as_str(&self) -> &str {
282        &self.0
283    }
284}
285
286impl FromStr for Tag {
287    type Err = miette::Error;
288
289    fn from_str(s: &str) -> Result<Self, Self::Err> {
290        let regex = regex!(r"[\w][\w.-]{0,127}");
291        regex
292            .is_match(s)
293            .then(|| Self(s.into()))
294            .ok_or_else(|| miette!("Invalid tag: {s}"))
295    }
296}
297
298impl std::fmt::Display for Tag {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        write!(f, "{}", &self.0)
301    }
302}
303
304impl TryFrom<&Reference> for Tag {
305    type Error = miette::Error;
306
307    fn try_from(value: &Reference) -> Result<Self, Self::Error> {
308        value
309            .tag()
310            .map(|tag| Self(tag.into()))
311            .ok_or_else(|| miette!("Reference {value} has no tag"))
312    }
313}
314
315impl<'de> Deserialize<'de> for Tag {
316    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
317    where
318        D: serde::Deserializer<'de>,
319    {
320        Self::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
321    }
322}
323
324impl Default for Tag {
325    fn default() -> Self {
326        Self(String::from("latest"))
327    }
328}
329
330impl From<Tag> for String {
331    fn from(value: Tag) -> Self {
332        value.0
333    }
334}
335
336impl From<&Tag> for String {
337    fn from(value: &Tag) -> Self {
338        value.0.clone()
339    }
340}